Summary
=======

Rajesh Pangare discovered an improper output neutralization vulnerability in
AIDE, an advanced intrusion detection system. An attacker can craft a malicious
filename by including terminal escape sequences to hide the addition or removal
of the file from the report and/or tamper with the log output. A local user
might exploit this to bypass the AIDE detection of malicious files.
Additionally the output of extended attribute key names and symbolic links
targets are also not properly neutralized.

Project
=======

AIDE (https://aide.github.io)

Affected versions
=================

AIDE <= 0.19.1

CVE ID
======

CVE-2025-54389

Proof of concept
================

    $ mkdir test
    $ touch test/$(echo -e 'malicious-file\033[1A')
    $ touch test/regular-testfile
    $ aide --config <(printf -- "database_in=file:/dev/null\nroot_prefix = 
./test\n/ R") --check
    Start timestamp: 2025-08-10 07:23:25 +0000 (AIDE 0.19.1)
    AIDE found differences between database and filesystem!!
    Root prefix: ./test

    Summary:
      Total number of entries:      3
      Added entries:                3
      Removed entries:              0
      Changed entries:              0

    ---------------------------------------------------
    Added entries:
    ---------------------------------------------------

    d++++++++++++++++++: /
    f++++++++++++++++++: /regular-testfile

    ---------------------------------------------------
    The attributes of the (uncompressed) database(s):
    ---------------------------------------------------

    /dev/null
     SHA256    : 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NM
                 pJWZG3hSuFU=
     SHA512    : z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXc
                 g/SpIdNs6c5H0NE8XYXysP+DGNKHfuwv
                 Y7kxvUdBeoGlODJ6+SfaPg==
     STRIBOG256: P1OaIT6XyALMIp1HTGqjKoJaNgsqkzqU
                 n9klII2c4bs=
     STRIBOG512: jpRdogmqhp8EVZKFKbyuRnnphzq3B7VT
                 FfVs65i+8Kc2L3FVKDVu6DzaXyqsTGrS
                 ujpxXBvNgcuOn5C/TBwaig==
     SHA512/256: xnK40e9W7Sirh8NiLFEUBpvdOte4+XN0
                 mNDAHs7wlno=
     SHA3-256  : p//G+L8e12ZRwUdWoGHWYvWA/03kO0n6
                 gtgKS4D4Q0o=
     SHA3-512  : pp9zzKI6msXItWfcGFp1bpfJghZP4lhZ
                 4NHcwUdcgKYVshI68fX5TBHj6UAsOsVY
                 9QAZnZW20+MBdYWGKB3NJg==


    End timestamp: 2025-08-10 07:23:25 +0000 (run time: 0m 0s)

The output correctly shows the addition of `regular-testfile` but misses the
addition of the malicious file.

This works because the terminal escape sequence moves the cursor one line up
before the new line character of the regular output moves the cursor back to
the beginning of the line containing the malicious file; the next added entry
then overwrites the line.

Please note that the number of added entries is unchanged in the report. For
this POC a user might notice the difference between the number of added entries
and the list of added entries shown in the report, but with a much higher number
of added files the deviation is likely overlooked.

Analysis
========

The vulnerability is caused by missing output neutralization before printing
the filename, symbolic link target or extended attribute key name to the report
and log output.

This allows a user to craft a filename, symbolic link target or extended
attribute key name including control characters to tamper with or overwrite
previous output.

Mitigation
==========

Upgrade to AIDE v0.19.2 [v0.19.2]

Alternatively apply one of the provided patches:

aide-0.19_cve-2025-54389_control_chars.patch for 0.19.1
aide-0.18_cve-2025-54389_control_chars.patch for 0.18.8 (backported for Debian 
Bookworm)
aide-0.17_cve-2025-54389_control_chars.patch for 0.17.4 (backported for Debian 
Bullseye)

If you cannot upgrade, the issue can be mitigated by configuring AIDE to write
the report output to a regular file (e.g. `report_url=file:/var/log/aide.log`)
or redirecting stdout to a regular file. Additionally consider redirecting the
log output written to `stderr` to a regular file. Mind to open the generated
files with a program that escapes terminal sequences correctly.

[v0.19.2] https://github.com/aide/aide/releases/tag/v0.19.2

Credit
======

The issue was reported by Rajesh Pangare.
diff --git before/doc/aide.1 after/doc/aide.1
index a7b9462..5d10e33 100644
--- before/doc/aide.1
+++ after/doc/aide.1
@@ -122,12 +122,25 @@ SIGUSR1 toggles the log_level between current and debug level.
 .PP
 .SH NOTES
 
+.IP "Checksum encoding"
+
 The checksums in the database and in the output are by default base64
 encoded (see also report_base16 option).
 To decode them you can use the following shell command:
 
 echo <encoded_checksum> | base64 \-d | hexdump \-v \-e '32/1 "%02x" "\\n"'
 
+.IP "Control characters"
+
+Control characters (00-31 and 127) are always escaped in log and plain report
+output. They are escaped by a literal backslash (\\) followed by exactly 3
+digits representing the character in octal notation (e.g. a newline is output
+as "\fB\\012\fR"). A literal backslash is not escaped unless it is followed by
+3 digits (0-9), in this case the literal backslash is escaped as
+"\fB\\134\fR". Reports in JSON format are escaped according to the JSON specs
+(e.g. a newline is output as "\fB\\b\fR" or an escape (\fBESC\fR) is output as
+"\fB\\u001b\fR")
+
 .PP
 .SH FILES
 
diff --git before/include/util.h after/include/util.h
index aaff780..d339740 100644
--- before/include/util.h
+++ after/include/util.h
@@ -51,6 +51,9 @@ int cmpurl(url_t*, url_t*);
 
 int contains_unsafe(const char*);
 
+char *strnesc(const char *, size_t);
+char *stresc(const char *);
+
 void decode_string(char*);
 
 char* encode_string(const char*);
diff --git before/src/aide.c after/src/aide.c
index b04e721..576fbd3 100644
--- before/src/aide.c
+++ after/src/aide.c
@@ -271,7 +271,8 @@ static void read_param(int argc,char**argv)
                 conf->limit=checked_malloc(strlen(optarg)+1);
                 strcpy(conf->limit,optarg);
                 if((conf->limit_crx=pcre_compile(conf->limit, PCRE_ANCHORED, &pcre_error, &pcre_erroffset, NULL)) == NULL) {
-                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %i: %s, conf->limit, pcre_erroffset, pcre_error)
+                    char * limit_safe = stresc(conf->limit);
+                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %i: %s, limit_safe, pcre_erroffset, pcre_error)
                 }
                 log_msg(LOG_LEVEL_INFO,_("(--limit): set limit to '%s'"), conf->limit);
             break;
@@ -572,7 +573,11 @@ int main(int argc,char**argv)
       rx_rule* rule = NULL;
       int match = check_rxtree(conf->check_path, conf->tree, &rule, conf->check_file_type, true);
       if (match < 0) {
-        fprintf(stdout, "[ ] %c '%s': outside of limit '%s'\n", get_restriction_char(conf->check_file_type), conf->check_path, conf->limit);
+        char* limit_safe = stresc(conf->limit);
+        char* path_safe = stresc(conf->check_path);
+        fprintf(stdout, "[ ] %c '%s': outside of limit '%s'\n", get_restriction_char(conf->check_file_type), path_safe, limit_safe);
+        free(path_safe);
+        free(limit_safe);
         exit(2);
       } else {
         exit(match?0:1);
diff --git before/src/gen_list.c after/src/gen_list.c
index 98b437c..7059899 100644
--- before/src/gen_list.c
+++ after/src/gen_list.c
@@ -510,7 +510,9 @@ int check_rxtree(char* filename,seltree* tree, rx_rule* *rule, RESTRICTION_TYPE
   int match = check_seltree(tree, filename, file_type, rule);
   if (dry_run) {
       char * str;
-      fprintf(stdout, "[%c] %c '%s': ", match?'X':' ', get_restriction_char(file_type), filename);
+      char* filename_safe = stresc(filename);
+      fprintf(stdout, "[%c] %c '%s': ", match?'X':' ', get_restriction_char(file_type), filename_safe);
+      free(filename_safe);
       if (match > 0) {
           char* attr_str;
           fprintf(stdout, "%s: '%s%s %s %s' (%s:%d: '%s')\n", get_rule_type_long_string(match), match == EQUAL_MATCH?"=":"", (*rule)->rx, str = get_restriction_string((*rule)->restriction), attr_str = diff_attributes(0, (*rule)->attr), (*rule)->config_filename, (*rule)->config_linenumber, (*rule)->config_line);
diff --git before/src/log.c after/src/log.c
index 5a25a36..5d7fa74 100644
--- before/src/log.c
+++ after/src/log.c
@@ -25,6 +25,7 @@
 
 #include "log.h"
 #include "locale-aide.h"
+#include "util.h"
 
 LOG_LEVEL prev_log_level = LOG_LEVEL_UNSET;
 LOG_LEVEL log_level = LOG_LEVEL_UNSET;
@@ -60,7 +61,7 @@ static void cache_line(LOG_LEVEL level, const char* format, va_list ap) {
 
     void * tmp = realloc(cached_lines, (ncachedlines+1) * sizeof(log_cache)); /* freed in log_cached_lines() */
     if (tmp == NULL) {
-        log_msg(LOG_LEVEL_ERROR, "realloc() failed: %s", strerror(errno));
+        fprintf(stderr, "%s: realloc: failed to allocate memory\n", log_level_array[LOG_LEVEL_ERROR-1].log_string);
         exit(EXIT_FAILURE);
     } else {
         cached_lines = tmp;
@@ -85,7 +86,12 @@ const char * get_log_level_name(LOG_LEVEL level) {
 
 static void log_cached_lines(void) {
     for(int i = 0; i < ncachedlines; ++i) {
-        log_msg(cached_lines[i].level, "%s", cached_lines[i].message);
+        LOG_LEVEL level = cached_lines[i].level;
+        if (level == LOG_LEVEL_ERROR || level <= log_level) {
+            char * msg_safe = stresc(cached_lines[i].message);
+            fprintf(stderr, "%s: %s\n", log_level_array[level-1].log_string, msg_safe);
+            free(msg_safe);
+        }
         free(cached_lines[i].message);
     }
     ncachedlines = 0;
@@ -96,9 +102,24 @@ static void vlog_msg(LOG_LEVEL level,const char* format, va_list ap) {
     FILE* url = stderr;
 
     if (level == LOG_LEVEL_ERROR || level <= log_level) {
-        fprintf(url, "%s: ", log_level_array[level-1].log_string );
-        vfprintf(url, format, ap);
-        fprintf(url, "\n");
+
+        va_list aq;
+        va_copy(aq, ap);
+        size_t n = vsnprintf(NULL, 0, format, aq) + 1;
+        va_end(aq);
+
+        int size = n * sizeof(char);
+        char *msg_unsafe = malloc(size);
+        if (msg_unsafe == NULL) {
+            fprintf(stderr, "%s: malloc: failed to allocate %d bytes of memory\n", log_level_array[LOG_LEVEL_ERROR-1].log_string, size);
+            exit(EXIT_FAILURE);
+        }
+
+        vsnprintf(msg_unsafe, n, format, ap);
+        char *msg_safe = stresc(msg_unsafe);
+        free(msg_unsafe);
+        fprintf(url, "%s: %s\n", log_level_array[level-1].log_string, msg_safe);
+        free(msg_safe);
     } else if (log_level == LOG_LEVEL_UNSET) {
         cache_line(level, format, ap);
     }
diff --git before/src/report.c after/src/report.c
index b342a0f..e3cee48 100644
--- before/src/report.c
+++ after/src/report.c
@@ -631,6 +631,7 @@ if ((conf->action&(DO_COMPARE|DO_DIFF) || (conf->action&DO_INIT && r->detailed_i
 #endif
             )) {
 
+    char *filename_safe = stresc(((node->checked&NODE_REMOVED)?node->old_data:node->new_data)->filename);
     if(r->summarize_changes) {
         int i;
         char* summary = checked_malloc ((report_attrs_order_length+1) * sizeof (char));
@@ -676,17 +677,18 @@ if ((conf->action&(DO_COMPARE|DO_DIFF) || (conf->action&DO_INIT && r->detailed_i
             }
         }
         summary[report_attrs_order_length]='\0';
-        report_printf(r, "\n%s: %s", summary, ((node->checked&NODE_REMOVED)?node->old_data:node->new_data)->filename);
+        report_printf(r, "\n%s: %s", summary, filename_safe);
         free(summary); summary=NULL;
     } else {
         if (node->checked&NODE_ADDED) {
-            report_printf(r,_("\nadded: %s"),(node->new_data)->filename);
+            report_printf(r,_("\nadded: %s"), filename_safe);
         } else if (node->checked&NODE_REMOVED) {
-            report_printf(r,_("\nremoved: %s"),(node->old_data)->filename);
+            report_printf(r,_("\nremoved: %s"), filename_safe);
         } else if (node->checked&NODE_CHANGED) {
-            report_printf(r,_("\nchanged: %s"),(node->new_data)->filename);
+            report_printf(r,_("\nchanged: %s"), filename_safe);
         }
     }
+    free(filename_safe);
             }
         } else {
             break; /* list sorted by report_level */
@@ -698,36 +700,49 @@ if ((conf->action&(DO_COMPARE|DO_DIFF) || (conf->action&DO_INIT && r->detailed_i
 static void print_attribute(REPORT_LEVEL report_level, db_line* oline, db_line* nline,
         DB_ATTR_TYPE attr, report_t *r, const char* name,
         DB_ATTR_TYPE report_attrs, DB_ATTR_TYPE added_attrs, DB_ATTR_TYPE removed_attrs) {
-    char **ovalue, **nvalue;
+    char **ovalues = NULL;
+    char **nvalues = NULL;
     int onumber, nnumber, olen, nlen, i, k, c;
     int p = (width_details-(4 + MAX_WIDTH_DETAILS_STRING))/2;
 
         if ( (attr&report_attrs && r->level >= report_level)
           || (report_attrs && attr&(added_attrs|removed_attrs) && r->level >= REPORT_LEVEL_ADDED_REMOVED_ATTRIBUTES) ) {
 
-            onumber=get_attribute_values(attr, oline, &ovalue, r);
-            nnumber=get_attribute_values(attr, nline, &nvalue, r);
+            onumber=get_attribute_values(attr, oline, &ovalues, r);
+            nnumber=get_attribute_values(attr, nline, &nvalues, r);
 
             i = 0;
             while (i<onumber || i<nnumber) {
-                olen = i<onumber?strlen(ovalue[i]):0;
-                nlen = i<nnumber?strlen(nvalue[i]):0;
+                char *ovalue = NULL;
+                char *nvalue = NULL;
+                olen = 0;
+                nlen = 0;
+                if (i<onumber){
+                    ovalue = stresc(ovalues[i]);
+                    olen = strlen(ovalue);
+                }
+                if (i<nnumber) {
+                    nvalue = stresc(nvalues[i]);
+                    nlen = strlen(nvalue);
+                }
                 k = 0;
                 while (olen-p*k >= 0 || nlen-p*k >= 0) {
                     c = k*(p-1);
                     if (!onumber) {
-                        report_printf(r," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[i][c]:"");
+                        report_printf(r," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[c]:"");
                     } else if (!nnumber) {
-                        report_printf(r," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[i][c]:"");
+                        report_printf(r," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:"");
                     } else {
-                        report_printf(r," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[i][c]:"", p-1, nlen-c>0?&nvalue[i][c]:"");
+                        report_printf(r," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[c]:"", p-1, nlen-c>0?&nvalue[c]:"");
                     }
                     k++;
                 }
                 ++i;
+                free(ovalue);
+                free(nvalue);
             }
-            for(i=0; i < onumber; ++i) { free(ovalue[i]); ovalue[i]=NULL; } free(ovalue); ovalue=NULL;
-            for(i=0; i < nnumber; ++i) { free(nvalue[i]); nvalue[i]=NULL; } free(nvalue); nvalue=NULL;
+            for(i=0; i < onumber; ++i) { free(ovalues[i]); ovalues[i]=NULL; } free(ovalues); ovalues=NULL;
+            for(i=0; i < nnumber; ++i) { free(nvalues[i]); nvalues[i]=NULL; } free(nvalues); nvalues=NULL;
         }
 }
 
@@ -760,7 +775,9 @@ static void print_dbline_attributes(REPORT_LEVEL report_level, db_line* oline, d
             if (file_type) {
                 report_printf(r, "%s: ", file_type);
             }
-            report_printf(r, "%s\n", (nline==NULL?oline:nline)->filename);
+            char *filename_safe = stresc((nline==NULL?oline:nline)->filename);
+            report_printf(r, "%s\n", filename_safe);
+            free(filename_safe);
         }
 
     for (int j=0; j < report_attrs_order_length; ++j) {
@@ -829,9 +846,11 @@ static void terse_report(seltree* node) {
             if (!(node->checked&(NODE_MOVED_IN|NODE_MOVED_OUT))){
                 if (r->level >= REPORT_LEVEL_LIST_ENTRIES
                   && (node->old_data->attr&~(r->ignore_removed_attrs))^(node->new_data->attr&~(r->ignore_added_attrs)) ) {
+                    char *entry_safe = stresc(node->old_data->filename);
                     char *str = NULL;
                     report_printf(r, "Entry %s in databases has different attributes: %s\n",
-                            node->old_data->filename,str= diff_attributes(node->old_data->attr&~(r->ignore_removed_attrs),node->new_data->attr&~(r->ignore_added_attrs)));
+                            entry_safe,str= diff_attributes(node->old_data->attr&~(r->ignore_removed_attrs),node->new_data->attr&~(r->ignore_added_attrs)));
+                    free(entry_safe);
                     free(str);
                 }
                 DB_ATTR_TYPE changed_attrs = (node->changed_attrs)&~(r->ignore_changed_attrs);
@@ -933,14 +952,18 @@ static void print_report_header() {
         if (r->level >= REPORT_LEVEL_SUMMARY) {
             int first = 1;
             if (conf->limit != NULL) {
-                report_printf(r, _("Limit: %s"), conf->limit);
+                char *limit_safe = stresc(conf->limit);
+                report_printf(r, _("Limit: %s"), limit_safe);
+                free(limit_safe);
                 first = 0;
             }
 
             if (conf->action&(DO_INIT|DO_COMPARE) && conf->root_prefix_length > 0) {
                 if (first) { first=0; }
                 else { report_printf(r," | "); }
-                report_printf(r, _("Root prefix: %s"),conf->root_prefix);
+                char *prefix_safe = stresc(conf->root_prefix);
+                report_printf(r, _("Root prefix: %s"), prefix_safe);
+                free(prefix_safe);
             }
 
             if (r->level != REPORT_LEVEL_CHANGED_ATTRIBUTES) {
diff --git before/src/util.c after/src/util.c
index 1826059..ba7359e 100644
--- before/src/util.c
+++ after/src/util.c
@@ -99,6 +99,40 @@ int cmpurl(url_t* u1,url_t* u2)
   return RETOK;
 };
 
+static size_t escape_str(const char *unescaped_str, char *str, size_t s) {
+    size_t n = 0;
+    size_t i = 0;
+    char c;
+    while (i < s && (c = unescaped_str[i])) {
+        if ((c >= 0 && (c < 0x1f || c == 0x7f)) ||
+            (c == '\\' && isdigit(unescaped_str[i+1])
+                       && isdigit(unescaped_str[i+2])
+                       && isdigit(unescaped_str[i+3])
+                ) ) {
+            if (str) { snprintf(&str[n], 5, "\\%03o", c); }
+            n += 4;
+        } else {
+            if (str) { str[n] = c; }
+            n++;
+        }
+        i++;
+    }
+    if (str) { str[n] = '\0'; }
+    n++;
+    return n;
+}
+
+char *strnesc(const char *unescaped_str, size_t s) {
+    int n = escape_str(unescaped_str, NULL, s);
+    char *str = checked_malloc(n);
+    escape_str(unescaped_str, str, s);
+    return str;
+}
+
+char *stresc(const char *unescaped_str) {
+    return strnesc(unescaped_str, strlen(unescaped_str));
+}
+
 /* Returns 1 if the string contains unsafe characters, 0 otherwise.  */
 int contains_unsafe (const char *s)
 {
diff --git before/doc/aide.1 after/doc/aide.1
index c68335f..4737f73 100644
--- before/doc/aide.1
+++ after/doc/aide.1
@@ -130,12 +130,25 @@ SIGUSR1 toggles the log_level between current and debug level.
 .PP
 .SH NOTES
 
+.IP "Checksum encoding"
+
 The checksums in the database and in the output are by default base64
 encoded (see also report_base16 option).
 To decode them you can use the following shell command:
 
 echo <encoded_checksum> | base64 \-d | hexdump \-v \-e '32/1 "%02x" "\\n"'
 
+.IP "Control characters"
+
+Control characters (00-31 and 127) are always escaped in log and plain report
+output. They are escaped by a literal backslash (\\) followed by exactly 3
+digits representing the character in octal notation (e.g. a newline is output
+as "\fB\\012\fR"). A literal backslash is not escaped unless it is followed by
+3 digits (0-9), in this case the literal backslash is escaped as
+"\fB\\134\fR". Reports in JSON format are escaped according to the JSON specs
+(e.g. a newline is output as "\fB\\b\fR" or an escape (\fBESC\fR) is output as
+"\fB\\u001b\fR")
+
 .PP
 .SH FILES
 
diff --git before/include/util.h after/include/util.h
index 897ac64..c22c9d6 100644
--- before/include/util.h
+++ after/include/util.h
@@ -57,6 +57,9 @@ int cmpurl(url_t*, url_t*);
 
 int contains_unsafe(const char*);
 
+char *strnesc(const char *, size_t);
+char *stresc(const char *);
+
 void decode_string(char*);
 
 char* encode_string(const char*);
diff --git before/src/aide.c after/src/aide.c
index f9821f4..aa8a569 100644
--- before/src/aide.c
+++ after/src/aide.c
@@ -282,7 +282,8 @@ static void read_param(int argc,char**argv)
                 if((conf->limit_crx=pcre2_compile((PCRE2_SPTR) conf->limit, PCRE2_ZERO_TERMINATED, PCRE2_UTF|PCRE2_ANCHORED, &pcre2_errorcode, &pcre2_erroffset, NULL)) == NULL) {
                     PCRE2_UCHAR pcre2_error[128];
                     pcre2_get_error_message(pcre2_errorcode, pcre2_error, 128);
-                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, conf->limit, pcre2_erroffset, pcre2_error)
+                    char * limit_safe = stresc(conf->limit);
+                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, limit_safe, pcre2_erroffset, pcre2_error)
 
                 }
                 conf->limit_md = pcre2_match_data_create_from_pattern(conf->limit_crx, NULL);
diff --git before/src/gen_list.c after/src/gen_list.c
index 9561443..729dff3 100644
--- before/src/gen_list.c
+++ after/src/gen_list.c
@@ -339,35 +339,39 @@ void print_match(char* filename, rx_rule *rule, match_result match, RESTRICTION_
     char * str;
     char* attr_str;
     char file_type = get_restriction_char(restriction);
+    char *filename_safe = stresc(filename);
     switch (match) {
         case RESULT_SELECTIVE_MATCH:
             str = get_restriction_string(rule->restriction);
             attr_str = diff_attributes(0, rule->attr);
-            fprintf(stdout, "[X] %c '%s': selective rule: '%s %s %s' (%s:%d: '%s%s%s')\n", file_type, filename, rule->rx, str, attr_str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
+            fprintf(stdout, "[X] %c '%s': selective rule: '%s %s %s' (%s:%d: '%s%s%s')\n", file_type, filename_safe, rule->rx, str, attr_str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
             free(attr_str);
             free(str);
             break;
         case RESULT_EQUAL_MATCH:
             str = get_restriction_string(rule->restriction);
             attr_str = diff_attributes(0, rule->attr);
-            fprintf(stdout, "[X] %c '%s': equal rule: '=%s %s %s' (%s:%d: '%s%s%s')\n", file_type, filename, rule->rx, str, attr_str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
+            fprintf(stdout, "[X] %c '%s': equal rule: '=%s %s %s' (%s:%d: '%s%s%s')\n", file_type, filename_safe, rule->rx, str, attr_str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
             free(attr_str);
             free(str);
             break;
         case RESULT_PARTIAL_MATCH:
         case RESULT_NO_MATCH:
             if (rule) {
-                fprintf(stdout, "[ ] %c '%s': negative rule: '!%s %s' (%s:%d: '%s%s%s')\n", file_type, filename, rule->rx, str = get_restriction_string(rule->restriction), rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
+                fprintf(stdout, "[ ] %c '%s': negative rule: '!%s %s' (%s:%d: '%s%s%s')\n", file_type, filename_safe, rule->rx, str = get_restriction_string(rule->restriction), rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"");
                 free(str);
             } else {
-                fprintf(stdout, "[ ] %c '%s': no matching rule\n", file_type, filename);
+                fprintf(stdout, "[ ] %c '%s': no matching rule\n", file_type, filename_safe);
             }
             break;
         case RESULT_PARTIAL_LIMIT_MATCH:
         case RESULT_NO_LIMIT_MATCH:
-            fprintf(stdout, "[ ] %c '%s': outside of limit '%s'\n", file_type, filename, conf->limit);
+            str = stresc(conf->limit);
+            fprintf(stdout, "[ ] %c '%s': outside of limit '%s'\n", file_type, filename_safe, str);
+            free(str);
             break;
     }
+    free(filename_safe);
 }
 
 /*
diff --git before/src/log.c after/src/log.c
index 3f741a6..ce5417c 100644
--- before/src/log.c
+++ after/src/log.c
@@ -30,6 +30,7 @@
 
 #include "log.h"
 #include "errorcodes.h"
+#include "util.h"
 
 LOG_LEVEL prev_log_level = LOG_LEVEL_UNSET;
 LOG_LEVEL log_level = LOG_LEVEL_UNSET;
@@ -118,7 +119,9 @@ static void log_cached_lines(void) {
     for(int i = 0; i < ncachedlines; ++i) {
         LOG_LEVEL level = cached_lines[i].level;
         if (level == LOG_LEVEL_ERROR || level <= log_level) {
-            fprintf(url, "%s: %s\n", log_level_array[level-1].log_string, cached_lines[i].message);
+            char * msg_safe = stresc(cached_lines[i].message);
+            fprintf(url, "%s: %s\n", log_level_array[level-1].log_string, msg_safe);
+            free(msg_safe);
         }
         free(cached_lines[i].message);
     }
@@ -135,9 +138,24 @@ static void vlog_msg(LOG_LEVEL level,const char* format, va_list ap) {
     FILE* url = stderr;
 
     if (level == LOG_LEVEL_ERROR || level <= log_level) {
-        fprintf(url, "%s: ", log_level_array[level-1].log_string );
-        vfprintf(url, format, ap);
-        fprintf(url, "\n");
+
+        va_list aq;
+        va_copy(aq, ap);
+        size_t n = vsnprintf(NULL, 0, format, aq) + 1;
+        va_end(aq);
+
+        int size = n * sizeof(char);
+        char *msg_unsafe = malloc(size);
+        if (msg_unsafe == NULL) {
+            fprintf(stderr, "%s: malloc: failed to allocate %d bytes of memory\n", log_level_array[LOG_LEVEL_ERROR-1].log_string, size);
+            exit(MEMORY_ALLOCATION_FAILURE);
+        }
+
+        vsnprintf(msg_unsafe, n, format, ap);
+        char *msg_safe = stresc(msg_unsafe);
+        free(msg_unsafe);
+        fprintf(url, "%s: %s\n", log_level_array[level-1].log_string, msg_safe);
+        free(msg_safe);
     } else if (log_level == LOG_LEVEL_UNSET) {
         cache_line(level, format, ap);
     }
diff --git before/src/report_json.c after/src/report_json.c
index 2fc77bc..47e7fe7 100644
--- before/src/report_json.c
+++ after/src/report_json.c
@@ -29,6 +29,8 @@
 #include "report.h"
 #include "seltree_struct.h"
 #include "stdbool.h"
+#include "string.h"
+#include "stdio.h"
 #include "url.h"
 
 #define JSON_FMT_ARRAY_BEGIN "%*c\"%s\": [\n"
@@ -57,12 +59,53 @@ static int _escape_json_string(const char *src, char *escaped_string) {
     int n = 0;
 
     for (i = 0; i < strlen(src); ++i) {
-        if (src[i] == '\\') {
-            if (escaped_string) { escaped_string[n] = '\\'; }
-            n++;
+        switch(src[i]) {
+            case '\n':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = 'n'; }
+                n++;
+                break;
+            case '\t':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = 't'; }
+                n++;
+                break;
+            case '\b':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = 'b'; }
+                n++;
+                break;
+            case '\f':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = 'f'; }
+                n++;
+                break;
+            case '\r':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = 'r'; }
+                n++;
+                break;
+            case '"':
+            case '\\':
+                if (escaped_string) { escaped_string[n] = '\\'; }
+                n++;
+                if (escaped_string) { escaped_string[n] = src[i]; }
+                n++;
+                break;
+            default:
+                if (src[i] >= 0 && (src[i] < 0x1f || src[i] == 0x7f)) {
+                    if (escaped_string) { snprintf(&escaped_string[n], 7, "\\u%04d", src[i]); }
+                    n += 6;
+                } else {
+                    if (escaped_string) { escaped_string[n] = src[i]; }
+                    n++;
+                }
         }
-        if (escaped_string) { escaped_string[n] = src[i]; }
-        n++;
     }
     if (escaped_string) { escaped_string[n] = '\0'; }
     n++;
@@ -302,9 +345,11 @@ static void print_report_diff_attrs_entries_json(report_t *report) {
         report_printf(report, JSON_FMT_OBJECT_BEGIN, 2, ' ', "different_attributes");
         for(int i = 0; i < report->num_diff_attrs_entries; ++i) {
             char *str = NULL;
+            char *escaped_filename = _get_escaped_json_string(report->diff_attrs_entries[i].entry);
             report_printf(report, i+1<report->num_diff_attrs_entries?JSON_FMT_STRING_COMMA:JSON_FMT_STRING_LAST , 4, ' ',
-                    report->diff_attrs_entries[i].entry,
+                    escaped_filename,
                     str= diff_attributes(report->diff_attrs_entries[i].old_attrs, report->diff_attrs_entries[i].new_attrs));
+            free(escaped_filename);
             free(str);
         }
         report->num_diff_attrs_entries = 0;
diff --git before/src/report_plain.c after/src/report_plain.c
index c87fc72..7d8a746 100644
--- before/src/report_plain.c
+++ after/src/report_plain.c
@@ -55,7 +55,9 @@ static char* _get_not_grouped_list_string(report_t *report) {
 static void _print_config_option(report_t *report, config_option option, const char* value) {
     if (first) { first=false; }
     else { report_printf(report," | "); }
-    report_printf(report, "%s: %s", config_options[option].report_string, value);
+    char *value_safe = stresc(value);
+    report_printf(report, "%s: %s", config_options[option].report_string, value_safe);
+    free(value_safe);
 }
 
 static void _print_report_option(report_t *report, config_option option, const char* value) {
@@ -63,37 +65,49 @@ static void _print_report_option(report_t *report, config_option option, const c
 }
 
 static void _print_attribute(report_t *report, db_line* oline, db_line* nline, ATTRIBUTE attribute) {
-    char **ovalue = NULL;
-    char **nvalue = NULL;
+    char **ovalues = NULL;
+    char **nvalues = NULL;
     int onumber, nnumber, i, c;
     int p = (width_details-(4 + MAX_WIDTH_DETAILS_STRING))/2;
 
     DB_ATTR_TYPE attr = ATTR(attribute);
     const char* name = attributes[attribute].details_string;
 
-    onumber=get_attribute_values(attr, oline, &ovalue, report);
-    nnumber=get_attribute_values(attr, nline, &nvalue, report);
+    onumber=get_attribute_values(attr, oline, &ovalues, report);
+    nnumber=get_attribute_values(attr, nline, &nvalues, report);
 
     i = 0;
     while (i<onumber || i<nnumber) {
-        int olen = i<onumber?strlen(ovalue[i]):0;
-        int nlen = i<nnumber?strlen(nvalue[i]):0;
+        char *ovalue = NULL;
+        char *nvalue = NULL;
+        int olen = 0;
+        int nlen = 0;
+        if (i<onumber){
+            ovalue = stresc(ovalues[i]);
+            olen = strlen(ovalue);
+        }
+        if (i<nnumber) {
+            nvalue = stresc(nvalues[i]);
+            nlen = strlen(nvalue);
+        }
         int k = 0;
         while (olen-p*k >= 0 || nlen-p*k >= 0) {
             c = k*(p-1);
             if (!onumber) {
-                report_printf(report," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[i][c]:"");
+                report_printf(report," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[c]:"");
             } else if (!nnumber) {
-                report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[i][c]:"");
+                report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:"");
             } else {
-                report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[i][c]:"", p-1, nlen-c>0?&nvalue[i][c]:"");
+                report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[c]:"", p-1, nlen-c>0?&nvalue[c]:"");
             }
             k++;
         }
         ++i;
+        free(ovalue);
+        free(nvalue);
     }
-    for(i=0; i < onumber; ++i) { free(ovalue[i]); ovalue[i]=NULL; } free(ovalue); ovalue=NULL;
-    for(i=0; i < nnumber; ++i) { free(nvalue[i]); nvalue[i]=NULL; } free(nvalue); nvalue=NULL;
+    for(i=0; i < onumber; ++i) { free(ovalues[i]); ovalues[i]=NULL; } free(ovalues); ovalues=NULL;
+    for(i=0; i < nnumber; ++i) { free(nvalues[i]); nvalues[i]=NULL; } free(nvalues); nvalues=NULL;
 }
 
 static void _print_database_attributes(report_t *report, db_line* db) {
@@ -136,19 +150,21 @@ static void print_report_summary_plain(report_t *report) {
 }
 
 static void print_line_plain(report_t* report, seltree* node) {
+    char *filename_safe = stresc(((node->checked&NODE_REMOVED)?node->old_data:node->new_data)->filename);
     if(report->summarize_changes) {
         char* summary = get_summarize_changes_string(report, node);
-        report_printf(report, "\n%s: %s", summary, ((node->checked&NODE_REMOVED)?node->old_data:node->new_data)->filename);
+        report_printf(report, "\n%s: %s", summary, filename_safe);
         free(summary); summary=NULL;
     } else {
         if (node->checked&NODE_ADDED) {
-            report_printf(report, _("\nadded: %s"),(node->new_data)->filename);
+            report_printf(report, _("\nadded: %s"),filename_safe);
         } else if (node->checked&NODE_REMOVED) {
-            report_printf(report, _("\nremoved: %s"),(node->old_data)->filename);
+            report_printf(report, _("\nremoved: %s"),filename_safe);
         } else if (node->checked&NODE_CHANGED) {
-            report_printf(report, _("\nchanged: %s"),(node->new_data)->filename);
+            report_printf(report, _("\nchanged: %s"),filename_safe);
         }
     }
+    free(filename_safe);
 }
 
 static void print_report_dbline_attributes_plain(report_t *report, db_line* oline, db_line* nline, DB_ATTR_TYPE report_attrs) {
@@ -158,7 +174,9 @@ static void print_report_dbline_attributes_plain(report_t *report, db_line* olin
         if (file_type) {
             report_printf(report, "%s: ", file_type);
         }
-        report_printf(report, "%s\n", (nline==NULL?oline:nline)->filename);
+        char *filename_safe = stresc((nline==NULL?oline:nline)->filename);
+        report_printf(report, "%s\n", filename_safe);
+        free(filename_safe);
 
         print_dbline_attrs(report, oline, nline, report_attrs, _print_attribute);
     }
@@ -195,9 +213,11 @@ static void print_report_details_plain(report_t *report, seltree* node) {
 static void print_report_diff_attrs_entries_plain(report_t *report) {
     for(int i = 0; i < report->num_diff_attrs_entries; ++i) {
         char *str = NULL;
+        char *entry_safe = stresc(report->diff_attrs_entries[i].entry);
         report_printf(report, "Entry %s in databases has different attributes: %s\n",
-                report->diff_attrs_entries[i].entry,
+                entry_safe,
                 str= diff_attributes(report->diff_attrs_entries[i].old_attrs, report->diff_attrs_entries[i].new_attrs));
+        free(entry_safe);
         free(str);
     }
     report->num_diff_attrs_entries = 0;
diff --git before/src/util.c after/src/util.c
index 87f6801..f5c5e60 100644
--- before/src/util.c
+++ after/src/util.c
@@ -105,6 +105,40 @@ int cmpurl(url_t* u1,url_t* u2)
   return RETOK;
 }
 
+static size_t escape_str(const char *unescaped_str, char *str, size_t s) {
+    size_t n = 0;
+    size_t i = 0;
+    char c;
+    while (i < s && (c = unescaped_str[i])) {
+        if ((c >= 0 && (c < 0x1f || c == 0x7f)) ||
+            (c == '\\' && isdigit(unescaped_str[i+1])
+                       && isdigit(unescaped_str[i+2])
+                       && isdigit(unescaped_str[i+3])
+                ) ) {
+            if (str) { snprintf(&str[n], 5, "\\%03o", c); }
+            n += 4;
+        } else {
+            if (str) { str[n] = c; }
+            n++;
+        }
+        i++;
+    }
+    if (str) { str[n] = '\0'; }
+    n++;
+    return n;
+}
+
+char *strnesc(const char *unescaped_str, size_t s) {
+    int n = escape_str(unescaped_str, NULL, s);
+    char *str = checked_malloc(n);
+    escape_str(unescaped_str, str, s);
+    return str;
+}
+
+char *stresc(const char *unescaped_str) {
+    return strnesc(unescaped_str, strlen(unescaped_str));
+}
+
 /* Returns 1 if the string contains unsafe characters, 0 otherwise.  */
 int contains_unsafe (const char *s)
 {
diff --git before/doc/aide.1 after/doc/aide.1
index c6b274c..8572ee2 100644
--- before/doc/aide.1
+++ after/doc/aide.1
@@ -158,12 +158,25 @@ Resize the progress bar (if enabled).
 .PP
 .SH NOTES
 
+.IP "Checksum encoding"
+
 The checksums in the database and in the output are by default base64
 encoded (see also report_base16 option).
 To decode them you can use the following shell command:
 
 echo <encoded_checksum> | base64 \-d | hexdump \-v \-e '32/1 "%02x" "\\n"'
 
+.IP "Control characters"
+
+Control characters (00-31 and 127) are always escaped in log and plain report
+output. They are escaped by a literal backslash (\\) followed by exactly 3
+digits representing the character in octal notation (e.g. a newline is output
+as "\fB\\012\fR"). A literal backslash is not escaped unless it is followed by
+3 digits (0-9), in this case the literal backslash is escaped as
+"\fB\\134\fR". Reports in JSON format are escaped according to the JSON specs
+(e.g. a newline is output as "\fB\\b\fR" or an escape (\fBESC\fR) is output as
+"\fB\\u001b\fR")
+
 .PP
 .SH FILES
 
diff --git before/include/util.h after/include/util.h
index d4c53fc..4340562 100644
--- before/include/util.h
+++ after/include/util.h
@@ -89,6 +89,9 @@ int cmpurl(url_t*, url_t*);
 
 int contains_unsafe(const char*);
 
+char *strnesc(const char *, size_t);
+char *stresc(const char *);
+
 void decode_string(char*);
 
 char* encode_string(const char*);
diff --git before/src/aide.c after/src/aide.c
index 2a5ec9c..622e108 100644
--- before/src/aide.c
+++ after/src/aide.c
@@ -302,7 +302,8 @@ static void read_param(int argc,char**argv)
                 if((conf->limit_crx=pcre2_compile((PCRE2_SPTR) conf->limit, PCRE2_ZERO_TERMINATED, PCRE2_UTF|PCRE2_ANCHORED, &pcre2_errorcode, &pcre2_erroffset, NULL)) == NULL) {
                     PCRE2_UCHAR pcre2_error[128];
                     pcre2_get_error_message(pcre2_errorcode, pcre2_error, 128);
-                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, conf->limit, pcre2_erroffset, pcre2_error)
+                    char * limit_safe = stresc(conf->limit);
+                    INVALID_ARGUMENT("--limit", error in regular expression '%s' at %zu: %s, limit_safe, pcre2_erroffset, pcre2_error)
 
                 }
                 conf->limit_md = pcre2_match_data_create_from_pattern(conf->limit_crx, NULL);
@@ -649,14 +650,16 @@ static void list_attribute(db_line* entry, ATTRIBUTE attribute) {
 
     i = 0;
     while (i<num) {
-        int olen = strlen(value[i]);
+        char *ovalue = stresc(value[i]);
+        int olen = strlen(ovalue);
         int k = 0;
         while (olen-p*k >= 0) {
             c = k*(p-1);
-            fprintf(stdout,"  %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&value[i][c]:"");
+            fprintf(stdout,"  %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:"");
             k++;
         }
         ++i;
+        free(ovalue);
     }
     for(i=0; i < num; ++i) { free(value[i]); value[i]=NULL; } free(value); value=NULL;
 }
@@ -810,7 +813,9 @@ int main(int argc,char**argv)
       db_entry_t entry;
       while((entry = db_readline(&(conf->database_in), false)).line != NULL) {
           log_msg(LOG_LEVEL_RULE, "\u252c process '%s' (filetype: %c)", (entry.line)->filename, get_f_type_char_from_perm((entry.line)->perm));
-          fprintf(stdout, "%s\n", (entry.line)->filename);
+          char *entry_safe = stresc((entry.line)->filename);
+          fprintf(stdout, "%s\n", entry_safe);
+          free(entry_safe);
           for (int j=0; j < report_attrs_order_length; ++j) {
               switch(report_attrs_order[j]) {
                   case attr_allhashsums:
diff --git before/src/gen_list.c after/src/gen_list.c
index 7564fa8..f836440 100644
--- before/src/gen_list.c
+++ after/src/gen_list.c
@@ -344,14 +344,14 @@ static DB_ATTR_TYPE get_different_attributes(db_line* l1, db_line* l2, DB_ATTR_T
 #define PRINT_RULE_MATCH(format, c, ...) \
     if (file.fs_type) { \
         fs_type_str = get_fs_type_string_from_magic(file.fs_type); \
-        fprintf(stdout, "[%c] %c=%s:%s: " format "\n", c, file_type, fs_type_str, file.name, __VA_ARGS__); \
+        fprintf(stdout, "[%c] %c=%s:%s: " format "\n", c, file_type, fs_type_str, filename_safe, __VA_ARGS__); \
         free(fs_type_str); \
     } else { \
-        fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, file.name, __VA_ARGS__); \
+        fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, filename_safe, __VA_ARGS__); \
     }
 #else
 #define PRINT_RULE_MATCH(format, c, ...) \
-    fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, file.name, __VA_ARGS__);
+    fprintf(stdout, "[%c] %c:%s: " format "\n", c, file_type, filename_safe, __VA_ARGS__);
 #endif
 
 void print_match(file_t file, match_t match) {
@@ -362,6 +362,8 @@ void print_match(file_t file, match_t match) {
     char *fs_type_str = NULL;
 #endif
     rx_rule *rule = match.rule;
+    char *filename_safe = stresc(file.name);
+    char *limit_safe = conf->limit?stresc(conf->limit):NULL;
     switch (match.result) {
         case RESULT_SELECTIVE_MATCH:
         case RESULT_EQUAL_MATCH:
@@ -379,7 +381,7 @@ void print_match(file_t file, match_t match) {
             break;
         case RESULT_NEGATIVE_PARENT_MATCH:
             str = get_restriction_string(rule->restriction);
-            PRINT_RULE_MATCH("parent directory '%.*s' matches %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', match.length, file.name, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"")
+            PRINT_RULE_MATCH("parent directory '%.*s' matches %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', match.length, filename_safe, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"")
             free(str);
             break;
         case RESULT_PARTIAL_MATCH:
@@ -387,21 +389,23 @@ void print_match(file_t file, match_t match) {
             PRINT_RULE_MATCH("%s", ' ', "no matching rule")
             break;
         case RESULT_PARTIAL_LIMIT_MATCH:
-            PRINT_RULE_MATCH("parital limit match (limit '%s')", ' ', conf->limit);
+            PRINT_RULE_MATCH("parital limit match (limit '%s')", ' ', limit_safe);
             break;
         case RESULT_PART_LIMIT_AND_NO_RECURSE_MATCH:
             if (rule) {
                 str = get_restriction_string(rule->restriction);
-                PRINT_RULE_MATCH("partial limit match (limit '%s') but %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', conf->limit, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"")
+                PRINT_RULE_MATCH("partial limit match (limit '%s') but %s: '%s%s %s' (%s:%d: '%s%s%s')", ' ', limit_safe, get_rule_type_long_string(rule->type), get_rule_type_char(rule->type), rule->rx, str, rule->config_filename, rule->config_linenumber, rule->config_line, rule->prefix?"', prefix: '":"", rule->prefix?rule->prefix:"")
                 free(str);
             } else {
-                PRINT_RULE_MATCH("partial limit match (limit '%s') but no matching rule", ' ', conf->limit)
+                PRINT_RULE_MATCH("partial limit match (limit '%s') but no matching rule", ' ', limit_safe)
             }
             break;
         case RESULT_NO_LIMIT_MATCH:
-            PRINT_RULE_MATCH("outside of limit '%s'", ' ', conf->limit);
+            PRINT_RULE_MATCH("outside of limit '%s'", ' ', limit_safe);
             break;
     }
+    free(filename_safe);
+    free(limit_safe);
 }
 
 /*
diff --git before/src/log.c after/src/log.c
index 9f4ea37..9bf5580 100644
--- before/src/log.c
+++ after/src/log.c
@@ -117,7 +117,9 @@ static void log_cached_lines(void) {
     for(int i = 0; i < ncachedlines; ++i) {
         LOG_LEVEL level = cached_lines[i].level;
         if (level == LOG_LEVEL_ERROR || level <= log_level) {
-            stderr_msg("%s: %s\n", get_log_string(level), cached_lines[i].message);
+            char *msg_safe = stresc(cached_lines[i].message);
+            stderr_msg("%s: %s\n", get_log_string(level), msg_safe);
+            free(msg_safe);
         }
         free(cached_lines[i].message);
     }
@@ -137,7 +139,23 @@ static void vlog_msg(LOG_LEVEL level,const char* format, va_list ap) {
         cache_line(level, format, ap);
     pthread_mutex_unlock(&log_mutex);
     } else if (level == LOG_LEVEL_ERROR || level <= log_level) {
-        vstderr_prefix_line(get_log_string(level), format, ap);
+        va_list aq;
+        va_copy(aq, ap);
+        int n = vsnprintf(NULL, 0, format, aq) + 1;
+        va_end(aq);
+
+        int size = n * sizeof(char);
+        char *msg_unsafe = malloc(size);
+        if (msg_unsafe == NULL) {
+            stderr_msg("%s: malloc: failed to allocate %d bytes of memory\n", get_log_string(LOG_LEVEL_ERROR), size);
+            exit(MEMORY_ALLOCATION_FAILURE);
+        }
+
+        vsnprintf(msg_unsafe, n, format, ap);
+        char *msg_safe = stresc(msg_unsafe);
+        free(msg_unsafe);
+        stderr_msg("%s: %s\n", get_log_string(level), msg_safe);
+        free(msg_safe);
     }
 }
 
diff --git before/src/progress.c after/src/progress.c
index ea85a68..940b84a 100644
--- before/src/progress.c
+++ after/src/progress.c
@@ -202,7 +202,7 @@ void progress_status(progress_state new_state, const char* data) {
             free(path);
             path = NULL;
             if (data) {
-                path = checked_strdup(data);
+                path = stresc(data);
             }
             break;
         case PROGRESS_SKIPPED:
diff --git before/src/report_json.c after/src/report_json.c
index 4a4f485..f9ed737 100644
--- before/src/report_json.c
+++ after/src/report_json.c
@@ -96,8 +96,13 @@ static int _escape_json_string(const char *src, char *escaped_string) {
                 n++;
                 break;
             default:
-                if (escaped_string) { escaped_string[n] = src[i]; }
-                n++;
+                if (src[i] >= 0 && (src[i] < 0x1f || src[i] == 0x7f)) {
+                    if (escaped_string) { snprintf(&escaped_string[n], 7, "\\u%04x", src[i]); }
+                    n += 6;
+                } else {
+                    if (escaped_string) { escaped_string[n] = src[i]; }
+                    n++;
+                }
         }
     }
     if (escaped_string) { escaped_string[n] = '\0'; }
diff --git before/src/report_plain.c after/src/report_plain.c
index 14f9b14..83cdd39 100644
--- before/src/report_plain.c
+++ after/src/report_plain.c
@@ -53,7 +53,9 @@ static char* _get_not_grouped_list_string(report_t *report) {
 static void _print_config_option(report_t *report, config_option option, const char* value) {
     if (first) { first=false; }
     else { report_printf(report," | "); }
-    report_printf(report, "%s: %s", config_options[option].report_string, value);
+    char *value_safe = stresc(value);
+    report_printf(report, "%s: %s", config_options[option].report_string, value_safe);
+    free(value_safe);
 }
 
 static void _print_report_option(report_t *report, config_option option, const char* value) {
@@ -61,37 +63,49 @@ static void _print_report_option(report_t *report, config_option option, const c
 }
 
 static void _print_attribute(report_t *report, db_line* oline, db_line* nline, ATTRIBUTE attribute) {
-    char **ovalue = NULL;
-    char **nvalue = NULL;
+    char **ovalues = NULL;
+    char **nvalues = NULL;
     int onumber, nnumber, i, c;
     int p = (conf->print_details_width-(4 + MAX_WIDTH_DETAILS_STRING))/2;
 
     DB_ATTR_TYPE attr = ATTR(attribute);
     const char* name = attributes[attribute].details_string;
 
-    onumber=get_attribute_values(attr, oline, &ovalue, report);
-    nnumber=get_attribute_values(attr, nline, &nvalue, report);
+    onumber=get_attribute_values(attr, oline, &ovalues, report);
+    nnumber=get_attribute_values(attr, nline, &nvalues, report);
 
     i = 0;
     while (i<onumber || i<nnumber) {
-        int olen = i<onumber?strlen(ovalue[i]):0;
-        int nlen = i<nnumber?strlen(nvalue[i]):0;
+        char *ovalue = NULL;
+        char *nvalue = NULL;
+        int olen = 0;
+        int nlen = 0;
+        if (i<onumber){
+            ovalue = stresc(ovalues[i]);
+            olen = strlen(ovalue);
+        }
+        if (i<nnumber) {
+            nvalue = stresc(nvalues[i]);
+            nlen = strlen(nvalue);
+        }
         int k = 0;
         while (olen-p*k >= 0 || nlen-p*k >= 0) {
             c = k*(p-1);
             if (!onumber) {
-                report_printf(report," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[i][c]:"");
+                report_printf(report," %-*s%c %-*c  %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, ' ', p-1, nlen-c>0?&nvalue[c]:"");
             } else if (!nnumber) {
-                report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[i][c]:"");
+                report_printf(report," %-*s%c %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p-1, olen-c>0?&ovalue[c]:"");
             } else {
-                report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[i][c]:"", p-1, nlen-c>0?&nvalue[i][c]:"");
+                report_printf(report," %-*s%c %-*.*s| %.*s\n", MAX_WIDTH_DETAILS_STRING, (i+k)?"":name, (i+k)?' ':':', p, p-1, olen-c>0?&ovalue[c]:"", p-1, nlen-c>0?&nvalue[c]:"");
             }
             k++;
         }
         ++i;
+        free(ovalue);
+        free(nvalue);
     }
-    for(i=0; i < onumber; ++i) { free(ovalue[i]); ovalue[i]=NULL; } free(ovalue); ovalue=NULL;
-    for(i=0; i < nnumber; ++i) { free(nvalue[i]); nvalue[i]=NULL; } free(nvalue); nvalue=NULL;
+    for(i=0; i < onumber; ++i) { free(ovalues[i]); ovalues[i]=NULL; } free(ovalues); ovalues=NULL;
+    for(i=0; i < nnumber; ++i) { free(nvalues[i]); nvalues[i]=NULL; } free(nvalues); nvalues=NULL;
 }
 
 static void _print_database_attributes(report_t *report, db_line* db) {
@@ -134,19 +148,21 @@ static void print_report_summary_plain(report_t *report) {
 }
 
 static void print_line_plain(report_t* report, char* filename, int node_checked, seltree* node) {
+    char *filename_safe = stresc(filename);
     if(report->summarize_changes) {
         char* summary = get_summarize_changes_string(report, node);
-        report_printf(report, "\n%s: %s", summary, filename);
+        report_printf(report, "\n%s: %s", summary, filename_safe);
         free(summary); summary=NULL;
     } else {
         if (node_checked&NODE_ADDED) {
-            report_printf(report, _("\nadded: %s"), filename);
+            report_printf(report, _("\nadded: %s"), filename_safe);
         } else if (node_checked&NODE_REMOVED) {
-            report_printf(report, _("\nremoved: %s"), filename);
+            report_printf(report, _("\nremoved: %s"), filename_safe);
         } else if (node_checked&NODE_CHANGED) {
-            report_printf(report, _("\nchanged: %s"), filename);
+            report_printf(report, _("\nchanged: %s"), filename_safe);
         }
     }
+    free(filename_safe);
 }
 
 static void print_report_dbline_attributes_plain(report_t *report, db_line* oline, db_line* nline, DB_ATTR_TYPE report_attrs) {
@@ -156,7 +172,9 @@ static void print_report_dbline_attributes_plain(report_t *report, db_line* olin
         if (line->perm) {
             report_printf(report, "%s: ", get_file_type_string(line->perm));
         }
-        report_printf(report, "%s\n", line->filename);
+        char *filename_safe = stresc(line->filename);
+        report_printf(report, "%s\n", filename_safe);
+        free(filename_safe);
 
         print_dbline_attrs(report, oline, nline, report_attrs, _print_attribute);
     }
@@ -193,9 +211,11 @@ static void print_report_details_plain(report_t *report, seltree* node) {
 static void print_report_diff_attrs_entries_plain(report_t *report) {
     for(int i = 0; i < report->num_diff_attrs_entries; ++i) {
         char *str = NULL;
+        char *entry_safe = stresc(report->diff_attrs_entries[i].entry);
         report_printf(report, "Entry %s in databases has different attributes: %s\n",
-                report->diff_attrs_entries[i].entry,
+                entry_safe,
                 str= diff_attributes(report->diff_attrs_entries[i].old_attrs, report->diff_attrs_entries[i].new_attrs));
+        free(entry_safe);
         free(str);
     }
     report->num_diff_attrs_entries = 0;
diff --git before/src/util.c after/src/util.c
index 900b3f4..2df2c19 100644
--- before/src/util.c
+++ after/src/util.c
@@ -143,6 +143,40 @@ int cmpurl(url_t* u1,url_t* u2)
   return RETOK;
 }
 
+static size_t escape_str(const char *unescaped_str, char *str, size_t s) {
+    size_t n = 0;
+    size_t i = 0;
+    char c;
+    while (i < s && (c = unescaped_str[i])) {
+        if ((c >= 0 && (c < 0x1f || c == 0x7f)) ||
+            (c == '\\' && isdigit(unescaped_str[i+1])
+                       && isdigit(unescaped_str[i+2])
+                       && isdigit(unescaped_str[i+3])
+                ) ) {
+            if (str) { snprintf(&str[n], 5, "\\%03o", c); }
+            n += 4;
+        } else {
+            if (str) { str[n] = c; }
+            n++;
+        }
+        i++;
+    }
+    if (str) { str[n] = '\0'; }
+    n++;
+    return n;
+}
+
+char *strnesc(const char *unescaped_str, size_t s) {
+    int n = escape_str(unescaped_str, NULL, s);
+    char *str = checked_malloc(n);
+    escape_str(unescaped_str, str, s);
+    return str;
+}
+
+char *stresc(const char *unescaped_str) {
+    return strnesc(unescaped_str, strlen(unescaped_str));
+}
+
 /* Returns 1 if the string contains unsafe characters, 0 otherwise.  */
 int contains_unsafe (const char *s)
 {

Attachment: signature.asc
Description: PGP signature

Reply via email to