CVE-2020-5253: Improper Privilege Management (CWE-269)
value: NetHack before version 3.6.0 allowed malicious use of escaping of characters in the configuration file (usually .nethackrc) which could be exploited. This bug is patched in NetHack 3.6.0.
CWE description: The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.
CVSS v3 base score: 9.8
Published: 2020-03-10T17:15Z
Repository: https://github.com/NetHack/NetHack
Language: C
Files changed: src/options.c
Commit date: 2011-08-04 02:41:44 +0000
Commit message: escapes() revamp

     Partial rewrite of escapes(), mostly changing its if-then-else
logic so that end-of-string can be checked once instead for each case.
The previous version had a bug if the input string ended with backslash
and one decimal digit (due to being lumped together with the handling
for trailing \X or \O).

Diff: diff --git a/src/options.c b/src/options.c
index c4a696aa0..49911f0f4 100644
--- a/src/options.c
+++ b/src/options.c
@@ -751,62 +751,76 @@ nmcpy(dest, src, maxlen)
  * include \n, \b, \t, \r, \xnnn (hex), \onnn (octal), \nnn (decimal).
  * The ^-prefix for control characters is also understood, and \[mM]
  * has the effect of 'meta'-ing the value which follows (so that the
  * alternate character set will be enabled).
  *
- * For 3.4.3 and earlier, ending with "\M", backslash, or caret prior
- * to terminating '\0' would pull that '\0' into the output and then
- * keep processing past it.  Now, a trailing escape will be handled
- * as if it was preceded with its own backslash and be kept as is,
- * with end of string terminator always honored as end of input.
+ * For 3.4.3 and earlier, input ending with "\M", backslash, or caret
+ * prior to terminating '\0' would pull that '\0' into the output and then
+ * keep processing past it, potentially overflowing the output buffer.
+ * Now, trailing \ or ^ will act like \\ or \^ and add '\\' or '^' to the
+ * output and stop there; trailing \M will fall through to \<other> and
+ * yield 'M', then stop.  Any \X or \O followed by something other than
+ * an appropriate digit will also fall through to \<other> and yield 'X'
+ * or 'O', plus stop if the non-digit is end-of-string.
  */
 STATIC_OVL void
 escapes(cp, tp)
 const char	*cp;
 char *tp;
 {
+    static NEARDATA const char
+	oct[] = "01234567", dec[] = "0123456789",
+	hex[] = "00112233445566778899aAbBcCdDeEfF";
+    const char *dp;
+    int cval, meta, dcount;
+
     while (*cp) {
-	int	cval = 0, meta = 0;
-
-	if (*cp == '\\' && cp[1] && index("mM", cp[1]) && cp[2]) {
-		meta = 1;
-		cp += 2;
-	}
-	if (*cp == '\\' && cp[1] && index("0123456789xXoO", cp[1]) && cp[2]) {
-	    NEARDATA const char hex[] = "00112233445566778899aAbBcCdDeEfF";
-	    const char *dp;
-	    int dcount = 0;
-
-	    cp++;
-	    if (*cp == 'x' || *cp == 'X')
-		for (++cp; *cp && (dp = index(hex, *cp)) && (dcount++ < 2); cp++)
-		    cval = (cval * 16) + ((int)(dp - hex) / 2);
-	    else if (*cp == 'o' || *cp == 'O')
-		for (++cp; *cp && (index("01234567",*cp)) && (dcount++ < 3); cp++)
-		    cval = (cval * 8) + (*cp - '0');
-	    else
-		for (; *cp && (index("0123456789",*cp)) && (dcount++ < 3); cp++)
-		    cval = (cval * 10) + (*cp - '0');
-	} else if (*cp == '\\' && cp[1]) {	/* C-style character escapes */
+	/* \M has to be followed by something to do meta conversion,
+	   otherwise it will just be \M which ultimately yields 'M' */
+	meta = (*cp == '\\' && (cp[1] == 'm' || cp[1] == 'M') && cp[2]);
+	if (meta) cp += 2;
+
+	cval = dcount = 0; /* for decimal, octal, hexadecimal cases */
+	if ((*cp != '\\' && *cp != '^') || !cp[1]) {
+	    /* simple character, or nothing left for \ or ^ to escape */
+	    cval = *cp++;
+	} else if (*cp == '^') {	/* expand control-character syntax */
+	    cval = (*++cp & 0x1f);
+	    ++cp;
+	/* remaining cases are all for backslash and we know cp[1] is not \0 */
+	} else if (index(dec, cp[1])) {
+	    ++cp;	/* move past backslash to first digit */
+	    do {
+		cval = (cval * 10) + (*cp - '0');
+	    } while (*++cp && index(dec, *cp) && ++dcount < 3);
+	} else if ((cp[1] == 'o' || cp[1] == 'O') &&
+		cp[2] && index(oct, cp[2])) {
+	    cp += 2;	/* move past backslash and 'O' */
+	    do {
+		cval = (cval * 8) + (*cp - '0');
+	    } while (*++cp && index(oct, *cp) && ++dcount < 3);
+	} else if ((cp[1] == 'x' || cp[1] == 'X') &&
+		cp[2] && (dp = index(hex, cp[2])) != 0) {
+	    cp += 2;	/* move past backslash and 'X' */
+	    do {
+		cval = (cval * 16) + ((int)(dp - hex) / 2);
+	    } while (*++cp && (dp = index(hex, *cp)) != 0 && ++dcount < 2);
+	} else {			/* C-style character escapes */
 	    switch (*++cp) {
 	    case '\\': cval = '\\'; break;
 	    case 'n': cval = '\n'; break;
 	    case 't': cval = '\t'; break;
 	    case 'b': cval = '\b'; break;
 	    case 'r': cval = '\r'; break;
 	    default: cval = *cp;
 	    }
-	    cp++;
-	} else if (*cp == '^' && cp[1]) { /* expand control-character syntax */
-	    cval = (*++cp & 0x1f);
-	    cp++;
-	} else
-	    cval = *cp++;
+	    ++cp;
+	}
 
 	if (meta)
 	    cval |= 0x80;
-	*tp++ = cval;
+	*tp++ = (char)cval;
     }
     *tp = '\0';
 }
 
 STATIC_OVL void

Vulnerable code:  * For 3.4.3 and earlier, ending with "\M", backslash, or caret prior
 * to terminating '\0' would pull that '\0' into the output and then
 * keep processing past it.  Now, a trailing escape will be handled
 * as if it was preceded with its own backslash and be kept as is,
 * with end of string terminator always honored as end of input.
	int	cval = 0, meta = 0;

	if (*cp == '\\' && cp[1] && index("mM", cp[1]) && cp[2]) {
		meta = 1;
		cp += 2;
	}
	if (*cp == '\\' && cp[1] && index("0123456789xXoO", cp[1]) && cp[2]) {
	    NEARDATA const char hex[] = "00112233445566778899aAbBcCdDeEfF";
	    const char *dp;
	    int dcount = 0;

	    cp++;
	    if (*cp == 'x' || *cp == 'X')
		for (++cp; *cp && (dp = index(hex, *cp)) && (dcount++ < 2); cp++)
		    cval = (cval * 16) + ((int)(dp - hex) / 2);
	    else if (*cp == 'o' || *cp == 'O')
		for (++cp; *cp && (index("01234567",*cp)) && (dcount++ < 3); cp++)
		    cval = (cval * 8) + (*cp - '0');
	    else
		for (; *cp && (index("0123456789",*cp)) && (dcount++ < 3); cp++)
		    cval = (cval * 10) + (*cp - '0');
	} else if (*cp == '\\' && cp[1]) {	/* C-style character escapes */
	    cp++;
	} else if (*cp == '^' && cp[1]) { /* expand control-character syntax */
	    cval = (*++cp & 0x1f);
	    cp++;
	} else
	    cval = *cp++;
	*tp++ = cval;
Fixed code:  * For 3.4.3 and earlier, input ending with "\M", backslash, or caret
 * prior to terminating '\0' would pull that '\0' into the output and then
 * keep processing past it, potentially overflowing the output buffer.
 * Now, trailing \ or ^ will act like \\ or \^ and add '\\' or '^' to the
 * output and stop there; trailing \M will fall through to \<other> and
 * yield 'M', then stop.  Any \X or \O followed by something other than
 * an appropriate digit will also fall through to \<other> and yield 'X'
 * or 'O', plus stop if the non-digit is end-of-string.
    static NEARDATA const char
	oct[] = "01234567", dec[] = "0123456789",
	hex[] = "00112233445566778899aAbBcCdDeEfF";
    const char *dp;
    int cval, meta, dcount;

	/* \M has to be followed by something to do meta conversion,
	   otherwise it will just be \M which ultimately yields 'M' */
	meta = (*cp == '\\' && (cp[1] == 'm' || cp[1] == 'M') && cp[2]);
	if (meta) cp += 2;

	cval = dcount = 0; /* for decimal, octal, hexadecimal cases */
	if ((*cp != '\\' && *cp != '^') || !cp[1]) {
	    /* simple character, or nothing left for \ or ^ to escape */
	    cval = *cp++;
	} else if (*cp == '^') {	/* expand control-character syntax */
	    cval = (*++cp & 0x1f);
	    ++cp;
	/* remaining cases are all for backslash and we know cp[1] is not \0 */
	} else if (index(dec, cp[1])) {
	    ++cp;	/* move past backslash to first digit */
	    do {
		cval = (cval * 10) + (*cp - '0');
	    } while (*++cp && index(dec, *cp) && ++dcount < 3);
	} else if ((cp[1] == 'o' || cp[1] == 'O') &&
		cp[2] && index(oct, cp[2])) {
	    cp += 2;	/* move past backslash and 'O' */
	    do {
		cval = (cval * 8) + (*cp - '0');
	    } while (*++cp && index(oct, *cp) && ++dcount < 3);
	} else if ((cp[1] == 'x' || cp[1] == 'X') &&
		cp[2] && (dp = index(hex, cp[2])) != 0) {
	    cp += 2;	/* move past backslash and 'X' */
	    do {
		cval = (cval * 16) + ((int)(dp - hex) / 2);
	    } while (*++cp && (dp = index(hex, *cp)) != 0 && ++dcount < 2);
	} else {			/* C-style character escapes */
	    ++cp;
	}
	*tp++ = (char)cval;