Skip to content

Commit 9ffddd7

Browse files
tniessenBethGriggs
authored andcommittedNov 1, 2022
inspector: harden IP address validation again
Use inet_pton() to parse IP addresses, which restricts IP addresses to a small number of well-defined formats. In particular, octal and hexadecimal number formats are not allowed, and neither are leading zeros. Also explicitly reject 0.0.0.0/8 and ::/128 as non-routable. Refs: https://hackerone.com/reports/1710652 CVE-ID: CVE-2022-43548 PR-URL: nodejs-private/node-private#354 Reviewed-by: Michael Dawson <midawson@redhat.com> Reviewed-by: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-by: Rich Trott <rtrott@gmail.com>
1 parent 7051ba4 commit 9ffddd7

File tree

2 files changed

+142
-16
lines changed

2 files changed

+142
-16
lines changed
 

‎src/inspector_socket.cc

+62-16
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "openssl/sha.h" // Sha-1 hash
88

9+
#include <algorithm>
910
#include <cstring>
1011
#include <map>
1112

@@ -162,25 +163,70 @@ static std::string TrimPort(const std::string& host) {
162163
}
163164

164165
static bool IsIPAddress(const std::string& host) {
165-
if (host.length() >= 4 && host.front() == '[' && host.back() == ']')
166-
return true;
167-
if (host.front() == '0') return false;
168-
uint_fast16_t accum = 0;
169-
uint_fast8_t quads = 0;
170-
bool empty = true;
171-
auto endOctet = [&accum, &quads, &empty](bool final = false) {
172-
return !empty && accum <= 0xff && ++quads <= 4 && final == (quads == 4) &&
173-
(empty = true) && !(accum = 0);
174-
};
175-
for (char c : host) {
176-
if (isdigit(c)) {
177-
if ((accum = (accum * 10) + (c - '0')) > 0xff) return false;
178-
empty = false;
179-
} else if (c != '.' || !endOctet()) {
166+
// TODO(tniessen): add CVEs to the following bullet points
167+
// To avoid DNS rebinding attacks, we are aware of the following requirements:
168+
// * the host name must be an IP address,
169+
// * the IP address must be routable, and
170+
// * the IP address must be formatted unambiguously.
171+
172+
// The logic below assumes that the string is null-terminated, so ensure that
173+
// we did not somehow end up with null characters within the string.
174+
if (host.find('\0') != std::string::npos) return false;
175+
176+
// All IPv6 addresses must be enclosed in square brackets, and anything
177+
// enclosed in square brackets must be an IPv6 address.
178+
if (host.length() >= 4 && host.front() == '[' && host.back() == ']') {
179+
// INET6_ADDRSTRLEN is the maximum length of the dual format (including the
180+
// terminating null character), which is the longest possible representation
181+
// of an IPv6 address: xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:ddd.ddd.ddd.ddd
182+
if (host.length() - 2 >= INET6_ADDRSTRLEN) return false;
183+
184+
// Annoyingly, libuv's implementation of inet_pton() deviates from other
185+
// implementations of the function in that it allows '%' in IPv6 addresses.
186+
if (host.find('%') != std::string::npos) return false;
187+
188+
// Parse the IPv6 address to ensure it is syntactically valid.
189+
char ipv6_str[INET6_ADDRSTRLEN];
190+
std::copy(host.begin() + 1, host.end() - 1, ipv6_str);
191+
ipv6_str[host.length()] = '\0';
192+
unsigned char ipv6[sizeof(struct in6_addr)];
193+
if (uv_inet_pton(AF_INET6, ipv6_str, ipv6) != 0) return false;
194+
195+
// The only non-routable IPv6 address is ::/128. It should not be necessary
196+
// to explicitly reject it because it will still be enclosed in square
197+
// brackets and not even macOS should make DNS requests in that case, but
198+
// history has taught us that we cannot be careful enough.
199+
// Note that RFC 4291 defines both "IPv4-Compatible IPv6 Addresses" and
200+
// "IPv4-Mapped IPv6 Addresses", which means that there are IPv6 addresses
201+
// (other than ::/128) that represent non-routable IPv4 addresses. However,
202+
// this translation assumes that the host is interpreted as an IPv6 address
203+
// in the first place, at which point DNS rebinding should not be an issue.
204+
if (std::all_of(ipv6, ipv6 + sizeof(ipv6), [](auto b) { return b == 0; })) {
180205
return false;
181206
}
207+
208+
// It is a syntactically valid and routable IPv6 address enclosed in square
209+
// brackets. No client should be able to misinterpret this.
210+
return true;
182211
}
183-
return endOctet(true);
212+
213+
// Anything not enclosed in square brackets must be an IPv4 address. It is
214+
// important here that inet_pton() accepts only the so-called dotted-decimal
215+
// notation, which is a strict subset of the so-called numbers-and-dots
216+
// notation that is allowed by inet_aton() and inet_addr(). This subset does
217+
// not allow hexadecimal or octal number formats.
218+
unsigned char ipv4[sizeof(struct in_addr)];
219+
if (uv_inet_pton(AF_INET, host.c_str(), ipv4) != 0) return false;
220+
221+
// The only strictly non-routable IPv4 address is 0.0.0.0, and macOS will make
222+
// DNS requests for this IP address, so we need to explicitly reject it. In
223+
// fact, we can safely reject all of 0.0.0.0/8 (see Section 3.2 of RFC 791 and
224+
// Section 3.2.1.3 of RFC 1122).
225+
// Note that inet_pton() stores the IPv4 address in network byte order.
226+
if (ipv4[0] == 0) return false;
227+
228+
// It is a routable IPv4 address in dotted-decimal notation.
229+
return true;
184230
}
185231

186232
// Constants for hybi-10 frame format.

‎test/cctest/test_inspector_socket.cc

+80
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,54 @@ TEST_F(InspectorSocketTest, HostIpTooManyOctetsChecked) {
925925
expect_handshake_failure();
926926
}
927927

928+
TEST_F(InspectorSocketTest, HostIpInvalidOctalOctetStartChecked) {
929+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
930+
"Host: 08.1.1.1:9229\r\n\r\n";
931+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
932+
INVALID_HOST_IP_REQUEST.length());
933+
expect_handshake_failure();
934+
}
935+
936+
TEST_F(InspectorSocketTest, HostIpInvalidOctalOctetMidChecked) {
937+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
938+
"Host: 1.09.1.1:9229\r\n\r\n";
939+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
940+
INVALID_HOST_IP_REQUEST.length());
941+
expect_handshake_failure();
942+
}
943+
944+
TEST_F(InspectorSocketTest, HostIpInvalidOctalOctetEndChecked) {
945+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
946+
"Host: 1.1.1.009:9229\r\n\r\n";
947+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
948+
INVALID_HOST_IP_REQUEST.length());
949+
expect_handshake_failure();
950+
}
951+
952+
TEST_F(InspectorSocketTest, HostIpLeadingZeroStartChecked) {
953+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
954+
"Host: 01.1.1.1:9229\r\n\r\n";
955+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
956+
INVALID_HOST_IP_REQUEST.length());
957+
expect_handshake_failure();
958+
}
959+
960+
TEST_F(InspectorSocketTest, HostIpLeadingZeroMidChecked) {
961+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
962+
"Host: 1.1.001.1:9229\r\n\r\n";
963+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
964+
INVALID_HOST_IP_REQUEST.length());
965+
expect_handshake_failure();
966+
}
967+
968+
TEST_F(InspectorSocketTest, HostIpLeadingZeroEndChecked) {
969+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
970+
"Host: 1.1.1.01:9229\r\n\r\n";
971+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
972+
INVALID_HOST_IP_REQUEST.length());
973+
expect_handshake_failure();
974+
}
975+
928976
TEST_F(InspectorSocketTest, HostIPNonRoutable) {
929977
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
930978
"Host: 0.0.0.0:9229\r\n\r\n";
@@ -933,4 +981,36 @@ TEST_F(InspectorSocketTest, HostIPNonRoutable) {
933981
expect_handshake_failure();
934982
}
935983

984+
TEST_F(InspectorSocketTest, HostIPv6NonRoutable) {
985+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
986+
"Host: [::]:9229\r\n\r\n";
987+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
988+
INVALID_HOST_IP_REQUEST.length());
989+
expect_handshake_failure();
990+
}
991+
992+
TEST_F(InspectorSocketTest, HostIPv6NonRoutableDual) {
993+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
994+
"Host: [::0.0.0.0]:9229\r\n\r\n";
995+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
996+
INVALID_HOST_IP_REQUEST.length());
997+
expect_handshake_failure();
998+
}
999+
1000+
TEST_F(InspectorSocketTest, HostIPv4InSquareBrackets) {
1001+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
1002+
"Host: [127.0.0.1]:9229\r\n\r\n";
1003+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
1004+
INVALID_HOST_IP_REQUEST.length());
1005+
expect_handshake_failure();
1006+
}
1007+
1008+
TEST_F(InspectorSocketTest, HostIPv6InvalidAbbreviation) {
1009+
const std::string INVALID_HOST_IP_REQUEST = "GET /json HTTP/1.1\r\n"
1010+
"Host: [:::1]:9229\r\n\r\n";
1011+
send_in_chunks(INVALID_HOST_IP_REQUEST.c_str(),
1012+
INVALID_HOST_IP_REQUEST.length());
1013+
expect_handshake_failure();
1014+
}
1015+
9361016
} // anonymous namespace

0 commit comments

Comments
 (0)
Please sign in to comment.