changeset 22723:781ee592b295

lib: printf_format_fix*() - Be over-strict in what format strings are allowed The checks could have been bypassed by some invalid format strings that were handled differently by the printf_format_fix*() code and libc. For example "%**%n" was passed through as ok, but glibc handled the %n in it. Found by cPanel Security Team.
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Tue, 17 Oct 2017 17:39:32 +0300
parents 2eba804a60a2
children a03e576ddfa0
files src/lib/printf-format-fix.c src/lib/test-printf-format-fix.c
diffstat 2 files changed, 118 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/src/lib/printf-format-fix.c	Thu Nov 09 10:52:12 2017 -0500
+++ b/src/lib/printf-format-fix.c	Tue Oct 17 17:39:32 2017 +0300
@@ -35,23 +35,108 @@
 static const char *
 printf_format_fix_noalloc(const char *format, size_t *len_r)
 {
-	static const char *printf_skip_chars = "# -+'I.*0123456789hlLjzt";
-	/* as a tiny optimization keep the most commonly used conversion
-	   modifiers first, so strchr() stops early. */
-	static const char *printf_allowed_conversions = "sudcioxXp%eEfFgGaA";
+	/* NOTE: This function is overly strict in what it accepts. Some
+	   format strings that are valid (and safe) in C99 will cause a panic
+	   here. This is because we don't really need to support the weirdest
+	   special cases, and we're also being extra careful not to pass
+	   anything to the underlying libc printf, which might treat the string
+	   differently than us and unexpectedly handling it as %n. For example
+	   "%**%n" with glibc. */
+
+	/* Allow only the standard C99 flags. There are also <'> and <I> flags,
+	   but we don't really need them. And at worst if they're not supported
+	   by the underlying printf, they could potentially be used to work
+	   around our restrictions. */
+	const char printf_flags[] = "#0- +";
+	/* As a tiny optimization keep the most commonly used conversion
+	   specifiers first, so strchr() stops early. */
+	static const char *printf_specifiers = "sudcixXpoeEfFgGaA";
 	const char *ret, *p, *p2;
+	char *flag;
 
 	p = ret = format;
 	while ((p2 = strchr(p, '%')) != NULL) {
+		const unsigned int start_pos = p2 - format;
+
 		p = p2+1;
-		while (*p != '\0' && strchr(printf_skip_chars, *p) != NULL)
+		if (*p == '%') {
+			/* we'll be strict and allow %% only when there are no
+			   optinal flags or modifiers. */
 			p++;
+			continue;
+		}
+		/* 1) zero or more flags. We'll add a further restriction that
+		   each flag can be used only once, since there's no need to
+		   use them more than once, and some implementations might
+		   add their own limits. */
+		bool printf_flags_seen[N_ELEMENTS(printf_flags)] = { FALSE, };
+		while (*p != '\0' &&
+		       (flag = strchr(printf_flags, *p)) != NULL) {
+			unsigned int flag_idx = flag - printf_flags;
+
+			if (printf_flags_seen[flag_idx]) {
+				i_panic("Duplicate %% flag '%c' starting at #%u in '%s'",
+					*p, start_pos, format);
+			}
+			printf_flags_seen[flag_idx] = TRUE;
+			p++;
+		}
 
-		if (*p == '\0') {
-			i_panic("%% modifier missing in '%s'", format);
-		} else if (strchr(printf_allowed_conversions, *p) != NULL) {
-			/* allow & ignore */
-		} else {
+		/* 2) Optional minimum field width */
+		if (*p == '*') {
+			/* We don't bother supporting "*m$" - it's not used
+			   anywhere and seems a bit dangerous. */
+			p++;
+		} else if (*p >= '1' && *p <= '9') {
+			/* Limit to 4 digits - we'll never want more than that.
+			   Some implementations might not handle long digits
+			   correctly, or maybe even could be used for DoS due
+			   to using too much CPU. */
+			unsigned int i = 0;
+			do {
+				p++;
+				if (++i > 4) {
+					i_panic("Too large minimum field width starting at #%u in '%s'",
+						start_pos, format);
+				}
+			} while (*p >= '0' && *p <= '9');
+		}
+
+		/* 3) Optional precision */
+		if (*p == '.') {
+			/* We don't bother supporting anything but numbers
+			   here. 999 should be long enough precision. */
+			unsigned int i = 0;
+			p++;
+			while (*p >= '0' && *p <= '9') {
+				if (++i > 3) {
+					i_panic("Too large precision starting at #%u in '%s'",
+						start_pos, format);
+				}
+				p++;
+			}
+		}
+
+		/* 4) Optional length modifier */
+		switch (*p) {
+		case 'h':
+			if (*++p == 'h')
+				p++;
+			break;
+		case 'l':
+			if (*++p == 'l')
+				p++;
+			break;
+		case 'L':
+		case 'j':
+		case 'z':
+		case 't':
+			p++;
+			break;
+		}
+
+		/* 5) conversion specifier */
+		if (*p == '\0' || strchr(printf_specifiers, *p) == NULL) {
 			switch (*p) {
 			case 'n':
 				i_panic("%%n modifier used");
@@ -60,8 +145,12 @@
 					i_panic("%%m used twice");
 				ret = fix_format_real(format, p-1, len_r);
 				break;
+			case '\0':
+				i_panic("Missing %% specifier starting at #%u in '%s'",
+					start_pos, format);
 			default:
-				i_panic("Unsupported %%%c modifier", *p);
+				i_panic("Unsupported 0x%02x specifier starting at #%u in '%s'",
+					*p, start_pos, format);
 			}
 		}
 		p++;
--- a/src/lib/test-printf-format-fix.c	Thu Nov 09 10:52:12 2017 -0500
+++ b/src/lib/test-printf-format-fix.c	Tue Oct 17 17:39:32 2017 +0300
@@ -17,7 +17,14 @@
 {
 	static const char *tests[] = {
 		"Hello world",
-		"Embedded %%, %u, %f, etc. are OK",
+		"Embedded %%, %u, %f, %s, etc. are OK",
+		"Allow %#0- +s flags",
+		"duplicate flags in different args %0-123s %0-123s",
+		"Minimum length %9999s",
+		"Precision %.999s",
+		"Precision %1.999s",
+		"Length modifiers %hd %hhd %ld %lld %Lg %jd %zd %td",
+		"Specifiers %s %u %d %c %i %x %X %p %o %e %E %f %F %g %G %a %A",
 		"%%doesn't cause confusion in %%m and %%n",
 	};
 	unsigned int i;
@@ -103,6 +110,16 @@
 		"%m allowed once, but not twice: %m",
 		"%m must not obscure a later %n",
 		"definitely can't have a tailing %",
+		"Evil %**%n",
+		"Evil %*#%99999$s",
+		"No weird %% with %0%",
+		"No duplicate modifiers %00s",
+		"Minimum length can't be too long %10000s",
+		"Minimum length doesn't support %*1$s",
+		"Precision can't be too long %.1000s",
+		"Precision can't be too long %1.1000s",
+		"Precision doesn't support %1.-1s",
+		"Precision doesn't support %1.*s",
 	};
 
 	if(stage >= N_ELEMENTS(fatals)) {