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)
 {
