Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions lib/requester/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -841,9 +841,11 @@ module.exports = {
* RFC 6052), IPv4-translated (::ffff:0:x.x.x.x, RFC 6145), and 6to4
* (2002::/16, RFC 3056) — all are unwrapped to IPv4 before CIDR matching.
*
* CIDR entries are parsed from restrictedAddresses on first call and cached
* on the networkOptions object; restrictedAddresses is treated as immutable
* after the first call — later mutations to CIDR entries are not picked up.
* CIDR entries, and bare-IP exact-match entries (promoted to a /32 or /128 CIDR so
* they get the same IPv6-embedding normalization below), are parsed from
* restrictedAddresses on first call and cached on the networkOptions object;
* restrictedAddresses is treated as immutable after the first call — later
* mutations are not picked up.
*
* @param {String} host
* @param {Object} networkOptions
Expand All @@ -853,7 +855,7 @@ module.exports = {
*/
isAddressRestricted (host, networkOptions) {
var restrictedAddresses = networkOptions.restrictedAddresses,
lowerHost, hasBrackets, strippedHost, compatEmbedded, addr, range;
lowerHost, hasBrackets, strippedHost, compatEmbedded, addr, range, parts;

if (!(host && restrictedAddresses)) { return false; }

Expand All @@ -863,24 +865,40 @@ module.exports = {
strippedHost = hasBrackets ? lowerHost.slice(1, -1) : lowerHost;

// exact match — check the original form, the stripped form, and (for bare IPv6
// addresses) the bracketed form so '[::1]' and '::1' entries are interchangeable
// addresses) the bracketed form so '[::1]' and '::1' entries are interchangeable.
// This is a fast path for hostname entries and already-canonicalized literals;
// IP entries are also covered below via the parsed CIDR/exact-match cache.
if (restrictedAddresses[lowerHost] || restrictedAddresses[strippedHost] ||
(!hasBrackets && _.includes(lowerHost, COLON) &&
restrictedAddresses['[' + lowerHost + ']'])) {
return true;
}

// lazy-init the parsed CIDR list on first call; cached for subsequent calls
// lazy-init the parsed address/CIDR list on first call; cached for subsequent calls
if (!networkOptions.restrictedCidrs) {
networkOptions.restrictedCidrs = Object.keys(restrictedAddresses)
.filter(function (key) {
return restrictedAddresses[key] && _.includes(key, '/');
return restrictedAddresses[key];
})
.reduce(function (acc, key) {
// silently ignore unparseable CIDR entries; the entry simply
// never matches rather than throwing on every lookup
try { acc.push(ipaddr.parseCIDR(key)); }
catch (e) { /* invalid CIDR entry, skip it */ }
var parsed,
bareKey = key;

try {
if (_.includes(key, '/')) {
acc.push(ipaddr.parseCIDR(key));
}
else {
if (key[0] === '[' && key[key.length - 1] === ']') {
bareKey = key.slice(1, -1);
}

parsed = ipaddr.parse(bareKey);
acc.push([parsed, parsed.kind() === 'ipv6' ? 128 : 32]);
}
}
// silently ignore entries that are neither a valid CIDR nor a valid IP
catch (e) { /* invalid restricted address entry, skip it */ }

return acc;
}, []);
Expand Down Expand Up @@ -910,6 +928,7 @@ module.exports = {

if (addr.kind() === 'ipv6') {
range = addr.range();
parts = addr.parts;

if (range === 'rfc6052' || range === 'rfc6145') {
// NAT64 (64:ff9b::/96, RFC 6052) and IPv4-translated (::ffff:0:x.x.x.x, RFC 6145)
Expand All @@ -920,6 +939,14 @@ module.exports = {
// 6to4 (2002::/16, RFC 3056) embeds the IPv4 address in bytes 2-5
addr = ipaddr.fromByteArray(addr.toByteArray().slice(2, 6));
}
else if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 &&
parts[3] === 0 && parts[4] === 0 && parts[5] === 0 &&
!(parts[6] === 0 && (parts[7] === 0 || parts[7] === 1))) {
// IPv4-compatible IPv6 in hex form (for example ::7f00:1 or ::7f01)
// keeps the IPv4 payload in the last 32 bits, but ipaddr.js reports
// it as generic IPv6 unicast instead of unwrapping it.
addr = ipaddr.fromByteArray(addr.toByteArray().slice(12));
}
}
}

Expand Down
92 changes: 91 additions & 1 deletion test/unit/requester-core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1165,6 +1165,79 @@
});
});

describe('bare exact-match entries get IPv6-embedding normalization too', function () {
// regression: a bare-IP entry (no '/') used to be checked via string equality
// only, so any non-identical encoding of the exact same address bypassed it,
// even when other CIDR entries existed elsewhere in the same restrictedAddresses
it('should block an IPv4-mapped IPv6 form of an exact-match-only entry', function () {
expect(requesterCore.isAddressRestricted('::ffff:169.254.169.254', {
restrictedAddresses: { '169.254.169.254': true }
})).to.be.true;
});

it('should block a bracketed IPv4-mapped IPv6 form of an exact-match-only entry', function () {
expect(requesterCore.isAddressRestricted('[::ffff:169.254.169.254]', {
restrictedAddresses: { '169.254.169.254': true }
})).to.be.true;
});

it('should block a NAT64-embedded form of an exact-match-only entry', function () {
expect(requesterCore.isAddressRestricted('64:ff9b::a9fe:a9fe', {
restrictedAddresses: { '169.254.169.254': true }
})).to.be.true;
});

it('should not block an unrelated IP against an exact-match-only list', function () {
expect(requesterCore.isAddressRestricted('8.8.8.8', {
restrictedAddresses: { '169.254.169.254': true }
})).to.be.false;
});

it('should block a mapped-IPv6 form of an exact entry even when a CIDR entry ' +
'for a different range is also present', function () {
expect(requesterCore.isAddressRestricted('::ffff:127.0.0.1', {
restrictedAddresses: { '127.0.0.1': true, '10.0.0.0/8': true }
})).to.be.true;
});

it('should still block a bare IPv6 exact-match entry via a differently-formatted ' +
'literal of the same address', function () {
expect(requesterCore.isAddressRestricted('0:0:0:0:0:0:0:1', {
restrictedAddresses: { '::1': true }
})).to.be.true;
});

it('should block an IPv4-compatible hex form of an exact-match-only entry', function () {
expect(requesterCore.isAddressRestricted('::7f00:1', {
restrictedAddresses: { '127.0.0.1': true }
})).to.be.true;
});

it('should block a low-32-bit hex form that begins with 0.0', function () {
expect(requesterCore.isAddressRestricted('::7f01', {
restrictedAddresses: { '0.0.127.1': true }
})).to.be.true;
});

it('should still block a bracketed IPv6 exact-match entry via an equivalent literal', function () {
expect(requesterCore.isAddressRestricted('0:0:0:0:0:0:0:1', {
restrictedAddresses: { '[::1]': true }
})).to.be.true;
});

it('should not treat a hostname entry as a parseable address (no throw, no match)', function () {
expect(requesterCore.isAddressRestricted('other.corp', {
restrictedAddresses: { 'internal.corp': true }
})).to.be.false;
});
it('should still exact-match a hostname entry', function () {
expect(requesterCore.isAddressRestricted('internal.corp', {
restrictedAddresses: { 'internal.corp': true }
})).to.be.true;
});

Check failure on line 1237 in test/unit/requester-core.test.js

View workflow job for this annotation

GitHub Actions / Lint

Block must not be padded by blank lines

});

describe('CIDR range matching', function () {
var ipv4CidrOpts;

Expand Down Expand Up @@ -1332,6 +1405,24 @@
restrictedAddresses: { '0.0.0.0/8': true }
})).to.be.true;
});

it('should block ::7f00:1 via a 127.0.0.0/8 CIDR', function () {
expect(requesterCore.isAddressRestricted('::7f00:1', {
restrictedAddresses: { '127.0.0.0/8': true }
})).to.be.true;
});

it('should block ::7f01 via a 0.0.0.0/8 CIDR', function () {
expect(requesterCore.isAddressRestricted('::7f01', {
restrictedAddresses: { '0.0.0.0/8': true }
})).to.be.true;
});

it('should not match ::1 against an unrelated 0.0.0.0/8 IPv4 CIDR', function () {
expect(requesterCore.isAddressRestricted('::1', {
restrictedAddresses: { '0.0.0.0/8': true }
})).to.be.false;
});
});

describe('NAT64-embedded IPv4 (64:ff9b::/96, RFC 6052)', function () {
Expand Down Expand Up @@ -1446,4 +1537,3 @@
});
});
});

Loading