Skip to content

Commit f1a4e70

Browse files
author
Jake Champion
committed
add support for validating ip addresses for the backend target so we can throw a js error if the ip address is invalid
1 parent 753ec18 commit f1a4e70

File tree

2 files changed

+194
-8
lines changed
  • c-dependencies/js-compute-runtime/builtins
  • integration-tests/js-compute/fixtures/dynamic-backend/bin

2 files changed

+194
-8
lines changed

c-dependencies/js-compute-runtime/builtins/backend.cpp

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,130 @@ std::vector<std::string_view> split(std::string_view string, char delimiter) {
2929
return result;
3030
}
3131

32+
// An IPv6 (normal) address has the format y:y:y:y:y:y:y:y, where y is
33+
// called a segment and can be any hexadecimal value between 0 and FFFF. The
34+
// segments are separated by colons, not periods. An IPv6 normal address
35+
// must have eight segments; however, a short form notation can be used in
36+
// the TS4500 management GUI for segments that are zero, or those that have
37+
// leading zeros.
38+
bool isValidNormalIPv6(std::string_view ip, int maxSegments = 8) {
39+
// if more than two consecutive colons then invalid
40+
// if no consecutive colons, then there must be 8 segments
41+
// if consecutive colons then there must be between 3 and 7 segments inclusive
42+
// each segment must be at most 4 hexadecimal digits
43+
auto emptySegments = 0;
44+
auto segments = split(ip, ':');
45+
if (segments.size() < 3) {
46+
return false;
47+
}
48+
if (segments.size() > maxSegments + 1) {
49+
return false;
50+
}
51+
auto firstSegmentIsEmpty = segments.front().length() == 0;
52+
auto lastSegmentIsEmpty = segments.back().length() == 0;
53+
for (auto segment : segments) {
54+
if (segment.length() == 0) {
55+
emptySegments++;
56+
if (emptySegments == 3) {
57+
if (segments.size() == 3) {
58+
return true;
59+
} else {
60+
return false;
61+
}
62+
}
63+
}
64+
if (segment.length() == 0) {
65+
continue;
66+
}
67+
if (segment.length() > 4) {
68+
return false;
69+
}
70+
71+
for (auto c : segment) {
72+
if (!std::isxdigit(c)) {
73+
return false;
74+
}
75+
}
76+
}
77+
78+
if (emptySegments == 0 && segments.size() != maxSegments) {
79+
return false;
80+
}
81+
82+
// This would indicate not enough segments are in the ip
83+
// E.G. :y
84+
if (emptySegments == 1 && firstSegmentIsEmpty && segments.size() != maxSegments) {
85+
return false;
86+
}
87+
88+
// E.G. y:y:y:y:y::y:y:y
89+
if (emptySegments == 2 && lastSegmentIsEmpty && segments.size() == maxSegments + 1) {
90+
return true;
91+
}
92+
// This would indicate the ip has multiple :: which is not allowed
93+
// E.G. y::y::y
94+
if (emptySegments > 1 && !firstSegmentIsEmpty && !lastSegmentIsEmpty) {
95+
return false;
96+
}
97+
98+
if (emptySegments && segments.size() > maxSegments) {
99+
return false;
100+
}
101+
return true;
102+
}
103+
104+
bool isValidIPv4(std::string_view ip) {
105+
auto sections = split(ip, '.');
106+
if (sections.size() != 4) {
107+
return false;
108+
}
109+
for (auto digits : sections) {
110+
if (digits.length() > 1 && digits.front() == '0') {
111+
return false;
112+
}
113+
int value;
114+
const std::from_chars_result result =
115+
std::from_chars(digits.data(), digits.data() + digits.size(), value);
116+
if (result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range) {
117+
return false;
118+
}
119+
if (value > 255) {
120+
return false;
121+
}
122+
}
123+
return true;
124+
}
125+
126+
// An IPv6 (dual) address combines an IPv6 and an IPv4 address and has the
127+
// following format: y:y:y:y:y:y:x.x.x.x. The IPv6 portion of the address
128+
// (indicated with y's) is always at the beginning, followed by the IPv4
129+
// portion (indicated with x's).
130+
// In the IPv6 portion of the address, y is called a segment and can be any
131+
// hexadecimal value between 0 and FFFF. The segments are separated by
132+
// colons, not periods. The IPv6 portion of the address must have six
133+
// segments but there is a short form notation for segments that are zero.
134+
// In the IPv4 portion of the address x is called an octet and must be a
135+
// decimal value between 0 and 255. The octets are separated by periods. The
136+
// IPv4 portion of the address must contain three periods and four octets.
137+
bool isValidDualIPv6(std::string_view ip) {
138+
std::size_t found = ip.find_last_of(':');
139+
auto ipv6 = ip[found - 1] == ':' ? ip.substr(0, found + 1) : ip.substr(0, found);
140+
auto ipv4 = ip.substr(found + 1);
141+
return isValidIPv4(ipv4) && isValidNormalIPv6(ipv6, 6);
142+
}
143+
144+
bool isValidIP(std::string_view ip) {
145+
if (ip.find(':') != std::string::npos) {
146+
if (ip.find('.') != std::string::npos) {
147+
return isValidDualIPv6(ip);
148+
} else {
149+
return isValidNormalIPv6(ip);
150+
}
151+
} else {
152+
return isValidIPv4(ip);
153+
}
154+
}
155+
32156
// A "host" is a "hostname" and an optional "port" in the format hostname:port
33157
// A "hostname" is between 1 and 255 octets -- https://www.rfc-editor.org/rfc/rfc1123#page-13
34158
// A "hostname" must start with a letter or digit -- https://www.rfc-editor.org/rfc/rfc1123#page-13
@@ -331,8 +455,11 @@ bool Backend::set_target(JSContext *cx, JSObject *backend, JS::HandleValue targe
331455
return false;
332456
}
333457

334-
// TODO: also allow IP addresses
335-
if (!isValidHost(targetString)) {
458+
if (targetString == "::") {
459+
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BACKEND_TARGET_INVALID);
460+
return false;
461+
}
462+
if (!isValidHost(targetString) && !isValidIP(targetString)) {
336463
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BACKEND_TARGET_INVALID);
337464
return false;
338465
}

integration-tests/js-compute/fixtures/dynamic-backend/bin/index.js

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -325,18 +325,41 @@ routes.set('/', () => {
325325
"fastly.com:80",
326326
"fastly.com:443",
327327
"fastly.com:65535",
328+
// Basic zero IPv4 address.
329+
"0.0.0.0",
330+
// Basic non-zero IPv4 address.
331+
"192.168.140.255",
332+
333+
// TODO: These are commented out as the hostcall currently yields an error of "invalid authority" when given an ipv6 address
334+
// Localhost IPv6.
335+
// "::1",
336+
// Fully expanded IPv6 address.
337+
// "fd7a:115c:a1e0:ab12:4843:cd96:626b:430b",
338+
// IPv6 with elided fields in the middle.
339+
// "fd7a:115c::626b:430b",
340+
// IPv6 with elided fields at the end.
341+
// "fd7a:115c:a1e0:ab12:4843:cd96::",
342+
// IPv6 with single elided field at the end.
343+
// "fd7a:115c:a1e0:ab12:4843:cd96:626b::",
344+
// IPv6 with single elided field in the middle.
345+
// "fd7a:115c:a1e0::4843:cd96:626b:430b",
346+
// IPv6 with the trailing 32 bits written as IPv4 dotted decimal. (4in6)
347+
// "::ffff:192.168.140.255",
348+
// IPv6 with capital letters.
349+
// "FD9E:1A04:F01D::1",
328350
];
329351
let i = 0;
330352
for (const target of targets) {
331353
let error = assertDoesNotThrow(() => {
332-
new Backend({ name: 'target-property-valid-host-'+i, target })
354+
console.log(target)
355+
new Backend({ name: 'target-property-valid-host-' + i, target })
333356
})
334357
if (error) { return error }
335358
i++;
336359
}
337360
return pass()
338361
});
339-
362+
340363
routes.set("/backend/constructor/parameter-target-property-invalid-host", async () => {
341364
const targets = [
342365
"-www.fastly.com",
@@ -348,11 +371,47 @@ routes.set('/', () => {
348371
"fastly.com:-1",
349372
"fastly.com:0",
350373
"fastly.com:65536",
374+
// IPv4 address in windows-style "print all the digits" form.
375+
// "010.000.015.001",
376+
// IPv4 address with a silly amount of leading zeros.
377+
// "000001.00000002.00000003.000000004",
378+
// 4-in-6 with octet with leading zero
379+
// "::ffff:1.2.03.4",
380+
// Basic zero IPv6 address.
381+
"::",
382+
// IPv6 with not enough fields
383+
"1:2:3:4:5:6:7",
384+
// IPv6 with too many fields
385+
"1:2:3:4:5:6:7:8:9",
386+
// IPv6 with 8 fields and a :: expander
387+
"1:2:3:4::5:6:7:8",
388+
// IPv6 with a field bigger than 2b
389+
"fe801::1",
390+
// IPv6 with non-hex values in field
391+
"fe80:tail:scal:e::",
392+
// IPv6 with a zone delimiter but no zone.
393+
"fe80::1%",
394+
// IPv6 (without ellipsis) with too many fields for trailing embedded IPv4.
395+
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:192.168.140.255",
396+
// IPv6 (with ellipsis) with too many fields for trailing embedded IPv4.
397+
"ffff::ffff:ffff:ffff:ffff:ffff:ffff:192.168.140.255",
398+
// IPv6 with invalid embedded IPv4.
399+
"::ffff:192.168.140.bad",
400+
// IPv6 with multiple ellipsis ::.
401+
"fe80::1::1",
402+
// IPv6 with invalid non hex/colon character.
403+
"fe80:1?:1",
404+
// IPv6 with truncated bytes after single colon.
405+
"fe80:",
406+
":::1",
407+
":0:1",
408+
":",
351409
];
352410
let i = 0;
353411
for (const target of targets) {
354412
let error = assertThrows(() => {
355-
new Backend({ name: 'target-property-invalid-host-'+i, target })
413+
console.log(target)
414+
new Backend({ name: 'target-property-invalid-host-' + i, target })
356415
}, TypeError, `Backend constructor: target does not contain a valid IPv4, IPv6, or hostname address`)
357416
if (error) { return error }
358417
i++;
@@ -604,7 +663,7 @@ routes.set('/', () => {
604663
// useSSL property
605664
{
606665
routes.set("/backend/constructor/parameter-useSSL-property-valid-boolean", async () => {
607-
const types = [{},[],Symbol(),1,"2"];
666+
const types = [{}, [], Symbol(), 1, "2"];
608667
for (const type of types) {
609668
let error = assertDoesNotThrow(() => {
610669
new Backend({ name: 'useSSL-property-valid-boolean' + type, target: 'a', useSSL: type })
@@ -805,7 +864,7 @@ routes.set('/', () => {
805864
// checkCertificate property
806865
{
807866
routes.set("/backend/constructor/parameter-checkCertificate-property-valid-boolean", async () => {
808-
const types = [{},[],Symbol(),1,"2"];
867+
const types = [{}, [], Symbol(), 1, "2"];
809868
for (const type of types) {
810869
let error = assertDoesNotThrow(() => {
811870
new Backend({ name: 'checkCertificate-property-valid-boolean' + type, target: 'a', checkCertificate: type })
@@ -865,7 +924,7 @@ routes.set('/', () => {
865924
return pass()
866925
});
867926
}
868-
927+
869928
// sniHostname property
870929
{
871930
routes.set("/backend/constructor/parameter-sniHostname-property-empty-string", async () => {

0 commit comments

Comments
 (0)