CVE-2014-3597: Improper Restriction of Operations within the Bounds of a Memory Buffer (CWE-119)
value: Multiple buffer overflows in the php_parserr function in ext/standard/dns.c in PHP before 5.4.32 and 5.5.x before 5.5.16 allow remote DNS servers to cause a denial of service (application crash) or possibly execute arbitrary code via a crafted DNS record, related to the dns_get_record function and the dn_expand function.  NOTE: this issue exists because of an incomplete fix for CVE-2014-4049.
CWE description: The product performs operations on a memory buffer, but it reads from or writes to a memory location outside the buffer's intended boundary. This may result in read or write operations on unexpected memory locations that could be linked to other variables, data structures, or internal program data.
Published: 2014-08-23T01:55Z
Repository: https://github.com/php/php-src
Language: C
Files changed: ext/standard/dns.c
Commit date: 2014-08-19 08:33:49 +0200
Commit message: Fixed Sec Bug #67717 segfault in dns_get_record CVE-2014-3597

Incomplete fix for CVE-2014-4049

Check possible buffer overflow
- pass real buffer end to dn_expand calls
- check buffer len before each read

Diff: diff --git a/ext/standard/dns.c b/ext/standard/dns.c
index 214a7dc7e92..0b5e69ca58c 100644
--- a/ext/standard/dns.c
+++ b/ext/standard/dns.c
@@ -410,12 +410,18 @@ PHP_FUNCTION(dns_check_record)
 }
 /* }}} */
 
 #if HAVE_FULL_DNS_FUNCS
 
+#define CHECKCP(n) do { \
+	if (cp + n > end) { \
+		return NULL; \
+	} \
+} while (0)
+
 /* {{{ php_parserr */
-static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int store, int raw, zval **subarray)
+static u_char *php_parserr(u_char *cp, u_char *end, querybuf *answer, int type_to_fetch, int store, int raw, zval **subarray)
 {
 	u_short type, class, dlen;
 	u_long ttl;
 	long n, i;
 	u_short s;
@@ -423,20 +429,22 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 	char name[MAXHOSTNAMELEN];
 	int have_v6_break = 0, in_v6_break = 0;
 
 	*subarray = NULL;
 
-	n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, sizeof(name) - 2);
+	n = dn_expand(answer->qb2, end, cp, name, sizeof(name) - 2);
 	if (n < 0) {
 		return NULL;
 	}
 	cp += n;
 
+	CHECKCP(10);
 	GETSHORT(type, cp);
 	GETSHORT(class, cp);
 	GETLONG(ttl, cp);
 	GETSHORT(dlen, cp);
+	CHECKCP(dlen);
 	if (type_to_fetch != T_ANY && type != type_to_fetch) {
 		cp += dlen;
 		return cp;
 	}
 
@@ -459,16 +467,18 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 		return cp;
 	}
 
 	switch (type) {
 		case DNS_T_A:
+			CHECKCP(4);
 			add_assoc_string(*subarray, "type", "A", 1);
 			snprintf(name, sizeof(name), "%d.%d.%d.%d", cp[0], cp[1], cp[2], cp[3]);
 			add_assoc_string(*subarray, "ip", name, 1);
 			cp += dlen;
 			break;
 		case DNS_T_MX:
+			CHECKCP(2);
 			add_assoc_string(*subarray, "type", "MX", 1);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "pri", n);
 			/* no break; */
 		case DNS_T_CNAME:
@@ -483,71 +493,79 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 			/* no break; */
 		case DNS_T_PTR:
 			if (type == DNS_T_PTR) {
 				add_assoc_string(*subarray, "type", "PTR", 1);
 			}
-			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
+			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
 			if (n < 0) {
 				return NULL;
 			}
 			cp += n;
 			add_assoc_string(*subarray, "target", name, 1);
 			break;
 		case DNS_T_HINFO:
 			/* See RFC 1010 for values */
 			add_assoc_string(*subarray, "type", "HINFO", 1);
+			CHECKCP(1);
 			n = *cp & 0xFF;
 			cp++;
+			CHECKCP(n);
 			add_assoc_stringl(*subarray, "cpu", (char*)cp, n, 1);
 			cp += n;
+			CHECKCP(1);
 			n = *cp & 0xFF;
 			cp++;
+			CHECKCP(n);
 			add_assoc_stringl(*subarray, "os", (char*)cp, n, 1);
 			cp += n;
 			break;
 		case DNS_T_TXT:
 			{
-				int ll = 0;
+				int l1 = 0, l2 = 0;
 				zval *entries = NULL;
 
 				add_assoc_string(*subarray, "type", "TXT", 1);
 				tp = emalloc(dlen + 1);
 				
 				MAKE_STD_ZVAL(entries);
 				array_init(entries);
 				
-				while (ll < dlen) {
-					n = cp[ll];
-					if ((ll + n) >= dlen) {
+				while (l1 < dlen) {
+					n = cp[l1];
+					if ((l1 + n) >= dlen) {
 						// Invalid chunk length, truncate
-						n = dlen - (ll + 1);
+						n = dlen - (l1 + 1);
+					}
+					if (n) {
+						memcpy(tp + l2 , cp + l1 + 1, n);
+						add_next_index_stringl(entries, cp + l1 + 1, n, 1);
 					}
-					memcpy(tp + ll , cp + ll + 1, n);
-					add_next_index_stringl(entries, cp + ll + 1, n, 1);
-					ll = ll + n + 1;
+					l1 = l1 + n + 1;
+					l2 = l2 + n;
 				}
-				tp[dlen] = '\0';
+				tp[l2] = '\0';
 				cp += dlen;
 
-				add_assoc_stringl(*subarray, "txt", tp, (dlen>0)?dlen - 1:0, 0);
+				add_assoc_stringl(*subarray, "txt", tp, l2, 0);
 				add_assoc_zval(*subarray, "entries", entries);
 			}
 			break;
 		case DNS_T_SOA:
 			add_assoc_string(*subarray, "type", "SOA", 1);
-			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) -2);
+			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) -2);
 			if (n < 0) {
 				return NULL;
 			}
 			cp += n;
 			add_assoc_string(*subarray, "mname", name, 1);
-			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) -2);
+			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) -2);
 			if (n < 0) {
 				return NULL;
 			}
 			cp += n;
 			add_assoc_string(*subarray, "rname", name, 1);
+			CHECKCP(5*4);
 			GETLONG(n, cp);
 			add_assoc_long(*subarray, "serial", n);
 			GETLONG(n, cp);
 			add_assoc_long(*subarray, "refresh", n);
 			GETLONG(n, cp);
@@ -557,10 +575,11 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 			GETLONG(n, cp);
 			add_assoc_long(*subarray, "minimum-ttl", n);
 			break;
 		case DNS_T_AAAA:
 			tp = (u_char*)name;
+			CHECKCP(8*2);
 			for(i=0; i < 8; i++) {
 				GETSHORT(s, cp);
 				if (s != 0) {
 					if (tp > (u_char *)name) {
 						in_v6_break = 0;
@@ -591,10 +610,11 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 			add_assoc_string(*subarray, "ipv6", name, 1);
 			break;
 		case DNS_T_A6:
 			p = cp;
 			add_assoc_string(*subarray, "type", "A6", 1);
+			CHECKCP(1);
 			n = ((int)cp[0]) & 0xFF;
 			cp++;
 			add_assoc_long(*subarray, "masklen", n);
 			tp = (u_char*)name;
 			if (n > 15) {
@@ -626,10 +646,11 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 					}
 				}
 				cp++;
 			}
 			for (i = (n + 8) / 16; i < 8; i++) {
+				CHECKCP(2);
 				GETSHORT(s, cp);
 				if (s != 0) {
 					if (tp > (u_char *)name) {
 						in_v6_break = 0;
 						tp[0] = ':';
@@ -655,49 +676,64 @@ static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int
 				tp++;
 			}
 			tp[0] = '\0';
 			add_assoc_string(*subarray, "ipv6", name, 1);
 			if (cp < p + dlen) {
-				n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
+				n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
 				if (n < 0) {
 					return NULL;
 				}
 				cp += n;
 				add_assoc_string(*subarray, "chain", name, 1);
 			}
 			break;
 		case DNS_T_SRV:
+			CHECKCP(3*2);
 			add_assoc_string(*subarray, "type", "SRV", 1);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "pri", n);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "weight", n);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "port", n);
-			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
+			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
 			if (n < 0) {
 				return NULL;
 			}
 			cp += n;
 			add_assoc_string(*subarray, "target", name, 1);
 			break;
 		case DNS_T_NAPTR:
+			CHECKCP(2*2);
 			add_assoc_string(*subarray, "type", "NAPTR", 1);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "order", n);
 			GETSHORT(n, cp);
 			add_assoc_long(*subarray, "pref", n);
+
+			CHECKCP(1);
 			n = (cp[0] & 0xFF);
-			add_assoc_stringl(*subarray, "flags", (char*)++cp, n, 1);
+			cp++;
+			CHECKCP(n);
+			add_assoc_stringl(*subarray, "flags", (char*)cp, n, 1);
 			cp += n;
+
+			CHECKCP(1);
 			n = (cp[0] & 0xFF);
-			add_assoc_stringl(*subarray, "services", (char*)++cp, n, 1);
+			cp++;
+			CHECKCP(n);
+			add_assoc_stringl(*subarray, "services", (char*)cp, n, 1);
 			cp += n;
+
+			CHECKCP(1);
 			n = (cp[0] & 0xFF);
-			add_assoc_stringl(*subarray, "regex", (char*)++cp, n, 1);
+			cp++;
+			CHECKCP(n);
+			add_assoc_stringl(*subarray, "regex", (char*)cp, n, 1);
 			cp += n;
-			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
+
+			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
 			if (n < 0) {
 				return NULL;
 			}
 			cp += n;
 			add_assoc_string(*subarray, "replacement", name, 1);
@@ -886,11 +922,11 @@ PHP_FUNCTION(dns_get_record)
 
 			/* YAY! Our real answers! */
 			while (an-- && cp && cp < end) {
 				zval *retval;
 
-				cp = php_parserr(cp, &answer, type_to_fetch, store_results, raw, &retval);
+				cp = php_parserr(cp, end, &answer, type_to_fetch, store_results, raw, &retval);
 				if (retval != NULL && store_results) {
 					add_next_index_zval(return_value, retval);
 				}
 			}
 
@@ -899,11 +935,11 @@ PHP_FUNCTION(dns_get_record)
 				 * Process when only requesting addtl so that we can skip through the section
 				 */
 				while (ns-- > 0 && cp && cp < end) {
 					zval *retval = NULL;
 
-					cp = php_parserr(cp, &answer, DNS_T_ANY, authns != NULL, raw, &retval);
+					cp = php_parserr(cp, end, &answer, DNS_T_ANY, authns != NULL, raw, &retval);
 					if (retval != NULL) {
 						add_next_index_zval(authns, retval);
 					}
 				}
 			}
@@ -911,11 +947,11 @@ PHP_FUNCTION(dns_get_record)
 			if (addtl) {
 				/* Additional records associated with authoritative name servers */
 				while (ar-- > 0 && cp && cp < end) {
 					zval *retval = NULL;
 
-					cp = php_parserr(cp, &answer, DNS_T_ANY, 1, raw, &retval);
+					cp = php_parserr(cp, end, &answer, DNS_T_ANY, 1, raw, &retval);
 					if (retval != NULL) {
 						add_next_index_zval(addtl, retval);
 					}
 				}
 			}

Vulnerable code: static u_char *php_parserr(u_char *cp, querybuf *answer, int type_to_fetch, int store, int raw, zval **subarray)
	n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, sizeof(name) - 2);
			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
				int ll = 0;
				while (ll < dlen) {
					n = cp[ll];
					if ((ll + n) >= dlen) {
						n = dlen - (ll + 1);
					memcpy(tp + ll , cp + ll + 1, n);
					add_next_index_stringl(entries, cp + ll + 1, n, 1);
					ll = ll + n + 1;
				tp[dlen] = '\0';
				add_assoc_stringl(*subarray, "txt", tp, (dlen>0)?dlen - 1:0, 0);
			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) -2);
			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) -2);
				n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
			add_assoc_stringl(*subarray, "flags", (char*)++cp, n, 1);
			add_assoc_stringl(*subarray, "services", (char*)++cp, n, 1);
			add_assoc_stringl(*subarray, "regex", (char*)++cp, n, 1);
			n = dn_expand(answer->qb2, answer->qb2+65536, cp, name, (sizeof name) - 2);
				cp = php_parserr(cp, &answer, type_to_fetch, store_results, raw, &retval);
					cp = php_parserr(cp, &answer, DNS_T_ANY, authns != NULL, raw, &retval);
					cp = php_parserr(cp, &answer, DNS_T_ANY, 1, raw, &retval);
Fixed code: #define CHECKCP(n) do { \
	if (cp + n > end) { \
		return NULL; \
	} \
} while (0)

static u_char *php_parserr(u_char *cp, u_char *end, querybuf *answer, int type_to_fetch, int store, int raw, zval **subarray)
	n = dn_expand(answer->qb2, end, cp, name, sizeof(name) - 2);
	CHECKCP(10);
	CHECKCP(dlen);
			CHECKCP(4);
			CHECKCP(2);
			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
			CHECKCP(1);
			CHECKCP(n);
			CHECKCP(1);
			CHECKCP(n);
				int l1 = 0, l2 = 0;
				while (l1 < dlen) {
					n = cp[l1];
					if ((l1 + n) >= dlen) {
						n = dlen - (l1 + 1);
					}
					if (n) {
						memcpy(tp + l2 , cp + l1 + 1, n);
						add_next_index_stringl(entries, cp + l1 + 1, n, 1);
					l1 = l1 + n + 1;
					l2 = l2 + n;
				tp[l2] = '\0';
				add_assoc_stringl(*subarray, "txt", tp, l2, 0);
			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) -2);
			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) -2);
			CHECKCP(5*4);
			CHECKCP(8*2);
			CHECKCP(1);
				CHECKCP(2);
				n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
			CHECKCP(3*2);
			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
			CHECKCP(2*2);

			CHECKCP(1);
			cp++;
			CHECKCP(n);
			add_assoc_stringl(*subarray, "flags", (char*)cp, n, 1);

			CHECKCP(1);
			cp++;
			CHECKCP(n);
			add_assoc_stringl(*subarray, "services", (char*)cp, n, 1);

			CHECKCP(1);
			cp++;
			CHECKCP(n);
			add_assoc_stringl(*subarray, "regex", (char*)cp, n, 1);

			n = dn_expand(answer->qb2, end, cp, name, (sizeof name) - 2);
				cp = php_parserr(cp, end, &answer, type_to_fetch, store_results, raw, &retval);
					cp = php_parserr(cp, end, &answer, DNS_T_ANY, authns != NULL, raw, &retval);
					cp = php_parserr(cp, end, &answer, DNS_T_ANY, 1, raw, &retval);