Vulnerability: In uClibc and uClibc-ng before 1.0.39, incorrect handling of special characters in domain names returned by DNS servers via gethostbyname, getaddrinfo, gethostbyaddr, and getnameinfo can lead to output of wrong hostnames (leading to domain hijacking) or injection into applications (leading to remote code execution, XSS, applications crashes, etc.). In other words, a validation step, which is expected in any stub resolver, does not occur.
Vulnerability ID: CVE-2021-43523
CWE: CWE-79
Severity score: 6.4
Project: https://github.com/wbx-github/uclibc-ng
Published: 2021-11-10
File: libc/inet/resolv.c
Commit message: libc/inet/resolv.c: add __hnbad to check DNS entries for validity…

… using the same rules glibc does

also call __hnbad in some places to check answers
Commit author: mirabilos

Vulnerable version (before fix):
 extern int __encode_answer(struct resolv_answer *a, 		int maxlen) attribute_hidden; extern void __open_nameservers(void) attribute_hidden; extern void __close_nameservers(void) attribute_hidden;  #define __encode_dotted(dotted,dest,maxlen) \ 	dn_comp((dotted), (dest), (maxlen), NULL, NULL) int __dns_lookup(const char *name, 	bool ends_with_dot; 	bool contains_dot; 	sockaddr46_t sa;  	fd = -1; 	lookup = NULL; int __dns_lookup(const char *name, 			goto fail1; 		} 		pos = HFIXEDSZ; 		for (j = 0; j < h.qdcount; j++) { 			DPRINTF("Skipping question %d at %d\n", j, pos); 			i = __length_question(packet + pos, packet_len - pos); int __dns_lookup(const char *name, 		DPRINTF("Decoding answer at pos %d\n", pos);  		first_answer = 1; 		a->dotted = NULL; 		for (j = 0; j < h.ancount; j++) { 			i = __decode_answer(packet, pos, packet_len, &ma); 			if (i < 0) { 				DPRINTF("failed decode %d\n", i); 				/* If the message was truncated but we have 				 * decoded some answers, pretend it's OK */ 				if (j && h.tc) 					break; 				goto try_next_server; 			} 			pos += i;  			if (first_answer) { 				ma.buf = a->buf; 				ma.buflen = a->buflen; int __dns_lookup(const char *name, 				++a->add_count; 			} 		}  		/* Success! */ 		DPRINTF("Answer name = |%s|\n", a->dotted); int gethostbyaddr_r(const void *addr, socklen_t addrlen, 		/* Decode CNAME into buf, feed it to __dns_lookup() again */ 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen); 		free(packet); 		if (i < 0) { 			*h_errnop = NO_RECOVERY; 			return -1; 		} int gethostbyaddr_r(const void *addr, socklen_t addrlen, 	if (a.atype == T_PTR) {	/* ADDRESS */ 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen); 		free(packet); 		result_buf->h_name = buf; 		result_buf->h_addrtype = type; 		result_buf->h_length = addrlen; int ns_name_pton(const char *src, u_char *dst, size_t dstsiz) } libc_hidden_def(ns_name_pton)  /*  * ns_name_unpack(msg, eom, src, dst, dstsiz)  *      Unpack a domain name from a message, source may be compressed.

Fixed version (after fix):
 extern int __encode_answer(struct resolv_answer *a, 		int maxlen) attribute_hidden; extern void __open_nameservers(void) attribute_hidden; extern void __close_nameservers(void) attribute_hidden; extern int __hnbad(const char *dotted) attribute_hidden;  #define __encode_dotted(dotted,dest,maxlen) \ 	dn_comp((dotted), (dest), (maxlen), NULL, NULL) int __dns_lookup(const char *name, 	bool ends_with_dot; 	bool contains_dot; 	sockaddr46_t sa; 	int num_answers;  	fd = -1; 	lookup = NULL; int __dns_lookup(const char *name, 			goto fail1; 		} 		pos = HFIXEDSZ; 		/*XXX TODO: check that question matches query (and qdcount==1?) */ 		for (j = 0; j < h.qdcount; j++) { 			DPRINTF("Skipping question %d at %d\n", j, pos); 			i = __length_question(packet + pos, packet_len - pos); int __dns_lookup(const char *name, 		DPRINTF("Decoding answer at pos %d\n", pos);  		first_answer = 1; 		num_answers = 0; 		a->dotted = NULL; 		for (j = 0; j < h.ancount; j++) { 			i = __decode_answer(packet, pos, packet_len, &ma); 			if (i < 0) { 				DPRINTF("failed decode %d\n", i); 				/* If the message was truncated but we have 				 * decoded some answers, pretend it's OK */ 				if (num_answers && h.tc) 					break; 				goto try_next_server; 			} 			pos += i;  			if (__hnbad(ma.dotted)) 				break; 			++num_answers; 			if (first_answer) { 				ma.buf = a->buf; 				ma.buflen = a->buflen; int __dns_lookup(const char *name, 				++a->add_count; 			} 		} 		if (!num_answers) { 			h_errno = NO_RECOVERY; 			goto fail1; 		}  		/* Success! */ 		DPRINTF("Answer name = |%s|\n", a->dotted); int gethostbyaddr_r(const void *addr, socklen_t addrlen, 		/* Decode CNAME into buf, feed it to __dns_lookup() again */ 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen); 		free(packet); 		if (i < 0 || __hnbad(buf)) { 			*h_errnop = NO_RECOVERY; 			return -1; 		} int gethostbyaddr_r(const void *addr, socklen_t addrlen, 	if (a.atype == T_PTR) {	/* ADDRESS */ 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen); 		free(packet); 		if (__hnbad(buf)) { 			*h_errnop = NO_RECOVERY; 			return -1; 		} 		result_buf->h_name = buf; 		result_buf->h_addrtype = type; 		result_buf->h_length = addrlen; int ns_name_pton(const char *src, u_char *dst, size_t dstsiz) } libc_hidden_def(ns_name_pton)  /*  * __hnbad(dotted)  *	Check whether a name is valid enough for DNS. The rules, as  *	laid down by glibc, are:  *	- printable input string  *	- converts to label notation  *	- each label only contains [0-9a-zA-Z_-], up to 63 octets  *	- first label doesn’t begin with ‘-’  *	This both is weaker than Unix hostnames (e.g. it allows  *	underscores and leading/trailing hyphen-minus) and stronger  *	than general (e.g. a leading “*.” is valid sometimes), take care.  * return:  *	0 if the name is ok  */ int __hnbad(const char *dotted) { 	unsigned char c, n, *cp; 	unsigned char buf[NS_MAXCDNAME];  	cp = (unsigned char *)dotted; 	while ((c = *cp++)) 		if (c < 0x21 || c > 0x7E) 			return (1); 	if (ns_name_pton(dotted, buf, sizeof(buf)) < 0) 		return (2); 	if (buf[0] > 0 && buf[1] == '-') 		return (3); 	cp = buf; 	while ((n = *cp++)) { 		if (n > 63) 			return (4); 		while (n--) { 			c = *cp++; 			if (c < '-' || 			    (c > '-' && c < '0') || 			    (c > '9' && c < 'A') || 			    (c > 'Z' && c < '_') || 			    (c > '_' && c < 'a') || 			    c > 'z') 				return (5); 		} 	} 	return (0); }  /*  * ns_name_unpack(msg, eom, src, dst, dstsiz)  *      Unpack a domain name from a message, source may be compressed.

Patch:
patch: @@ -409,6 +409,7 @@ extern int __encode_answer(struct resolv_answer *a,
 		int maxlen) attribute_hidden;
 extern void __open_nameservers(void) attribute_hidden;
 extern void __close_nameservers(void) attribute_hidden;
+extern int __hnbad(const char *dotted) attribute_hidden;
 
 #define __encode_dotted(dotted,dest,maxlen) \
 	dn_comp((dotted), (dest), (maxlen), NULL, NULL)
@@ -1093,6 +1094,7 @@ int __dns_lookup(const char *name,
 	bool ends_with_dot;
 	bool contains_dot;
 	sockaddr46_t sa;
+	int num_answers;
 
 	fd = -1;
 	lookup = NULL;
@@ -1335,6 +1337,7 @@ int __dns_lookup(const char *name,
 			goto fail1;
 		}
 		pos = HFIXEDSZ;
+		/*XXX TODO: check that question matches query (and qdcount==1?) */
 		for (j = 0; j < h.qdcount; j++) {
 			DPRINTF("Skipping question %d at %d\n", j, pos);
 			i = __length_question(packet + pos, packet_len - pos);
@@ -1349,19 +1352,23 @@ int __dns_lookup(const char *name,
 		DPRINTF("Decoding answer at pos %d\n", pos);
 
 		first_answer = 1;
+		num_answers = 0;
 		a->dotted = NULL;
 		for (j = 0; j < h.ancount; j++) {
 			i = __decode_answer(packet, pos, packet_len, &ma);
 			if (i < 0) {
 				DPRINTF("failed decode %d\n", i);
 				/* If the message was truncated but we have
 				 * decoded some answers, pretend it's OK */
-				if (j && h.tc)
+				if (num_answers && h.tc)
 					break;
 				goto try_next_server;
 			}
 			pos += i;
 
+			if (__hnbad(ma.dotted))
+				break;
+			++num_answers;
 			if (first_answer) {
 				ma.buf = a->buf;
 				ma.buflen = a->buflen;
@@ -1391,6 +1398,10 @@ int __dns_lookup(const char *name,
 				++a->add_count;
 			}
 		}
+		if (!num_answers) {
+			h_errno = NO_RECOVERY;
+			goto fail1;
+		}
 
 		/* Success! */
 		DPRINTF("Answer name = |%s|\n", a->dotted);
@@ -2357,7 +2368,7 @@ int gethostbyaddr_r(const void *addr, socklen_t addrlen,
 		/* Decode CNAME into buf, feed it to __dns_lookup() again */
 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen);
 		free(packet);
-		if (i < 0) {
+		if (i < 0 || __hnbad(buf)) {
 			*h_errnop = NO_RECOVERY;
 			return -1;
 		}
@@ -2366,6 +2377,10 @@ int gethostbyaddr_r(const void *addr, socklen_t addrlen,
 	if (a.atype == T_PTR) {	/* ADDRESS */
 		i = __decode_dotted(packet, a.rdoffset, packet_len, buf, buflen);
 		free(packet);
+		if (__hnbad(buf)) {
+			*h_errnop = NO_RECOVERY;
+			return -1;
+		}
 		result_buf->h_name = buf;
 		result_buf->h_addrtype = type;
 		result_buf->h_length = addrlen;
@@ -2930,6 +2945,51 @@ int ns_name_pton(const char *src, u_char *dst, size_t dstsiz)
 }
 libc_hidden_def(ns_name_pton)
 
+/*
+ * __hnbad(dotted)
+ *	Check whether a name is valid enough for DNS. The rules, as
+ *	laid down by glibc, are:
+ *	- printable input string
+ *	- converts to label notation
+ *	- each label only contains [0-9a-zA-Z_-], up to 63 octets
+ *	- first label doesn’t begin with ‘-’
+ *	This both is weaker than Unix hostnames (e.g. it allows
+ *	underscores and leading/trailing hyphen-minus) and stronger
+ *	than general (e.g. a leading “*.” is valid sometimes), take care.
+ * return:
+ *	0 if the name is ok
+ */
+int __hnbad(const char *dotted)
+{
+	unsigned char c, n, *cp;
+	unsigned char buf[NS_MAXCDNAME];
+
+	cp = (unsigned char *)dotted;
+	while ((c = *cp++))
+		if (c < 0x21 || c > 0x7E)
+			return (1);
+	if (ns_name_pton(dotted, buf, sizeof(buf)) < 0)
+		return (2);
+	if (buf[0] > 0 && buf[1] == '-')
+		return (3);
+	cp = buf;
+	while ((n = *cp++)) {
+		if (n > 63)
+			return (4);
+		while (n--) {
+			c = *cp++;
+			if (c < '-' ||
+			    (c > '-' && c < '0') ||
+			    (c > '9' && c < 'A') ||
+			    (c > 'Z' && c < '_') ||
+			    (c > '_' && c < 'a') ||
+			    c > 'z')
+				return (5);
+		}
+	}
+	return (0);
+}
+
 /*
  * ns_name_unpack(msg, eom, src, dst, dstsiz)
  *      Unpack a domain name from a message, source may be compressed.
path: libc/inet/resolv.c