Skip to content

Commit 780911d

Browse files
committed
fix: reject IPv4 trust via mapped IPv6 subnets with a short prefix
Ref: GHSA-jqcg-44mw-7w3h
1 parent 92e103e commit 780911d

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

‎index.js‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,13 @@ function trustMulti (subnets) {
256256
if (!isip(addr)) return false
257257

258258
var ip = parseip(addr)
259+
260+
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
261+
// Canonicalize IPv4-mapped candidates to IPv4 so a mapped address
262+
// cannot bypass the cross-family guard via the same-family path
263+
ip = ip.toIPv4Address()
264+
}
265+
259266
var ipconv
260267
var kind = ip.kind()
261268

@@ -272,6 +279,12 @@ function trustMulti (subnets) {
272279
continue
273280
}
274281

282+
if (subnetkind !== 'ipv4' && !(subnetrange >= 96 && subnetip.isIPv4MappedAddress())) {
283+
// IPv6 subnet only spans IPv4 when it is a mapped subnet whose
284+
// prefix covers the ::ffff: marker; otherwise it must not match IPv4
285+
continue
286+
}
287+
275288
if (!ipconv) {
276289
// Convert IP to match subnet IP kind
277290
ipconv = subnetkind === 'ipv4'
@@ -280,6 +293,9 @@ function trustMulti (subnets) {
280293
}
281294

282295
trusted = ipconv
296+
} else if (kind === 'ipv6' && subnetip.isIPv4MappedAddress()) {
297+
// A native IPv6 candidate cannot match an IPv4-mapped subnet
298+
continue
283299
}
284300

285301
if (trusted.match(subnetip, subnetrange)) {
@@ -308,6 +324,13 @@ function trustSingle (subnet) {
308324
if (!isip(addr)) return false
309325

310326
var ip = parseip(addr)
327+
328+
if (ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
329+
// Canonicalize IPv4-mapped candidates to IPv4 so a mapped address
330+
// cannot bypass the cross-family guard via the same-family path
331+
ip = ip.toIPv4Address()
332+
}
333+
311334
var kind = ip.kind()
312335

313336
if (kind !== subnetkind) {
@@ -316,10 +339,19 @@ function trustSingle (subnet) {
316339
return false
317340
}
318341

342+
if (!subnetisipv4 && !(subnetrange >= 96 && subnetip.isIPv4MappedAddress())) {
343+
// IPv6 subnet only spans IPv4 when it is a mapped subnet whose
344+
// prefix covers the ::ffff: marker; otherwise it must not match IPv4
345+
return false
346+
}
347+
319348
// Convert IP to match subnet IP kind
320349
ip = subnetisipv4
321350
? ip.toIPv4Address()
322351
: ip.toIPv4MappedAddress()
352+
} else if (kind === 'ipv6' && subnetip.isIPv4MappedAddress()) {
353+
// A native IPv6 candidate cannot match an IPv4-mapped subnet
354+
return false
323355
}
324356

325357
return ip.match(subnetip, subnetrange)

‎test/test.js‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,125 @@ describe('proxyaddr.compile(trust)', function () {
499499
})
500500
})
501501

502+
describe('proxyaddr.compile(trust) IPv4-mapped IPv6 subnets', function () {
503+
// @see https://github.com/jshttp/proxy-addr/security/advisories/GHSA-jqcg-44mw-7w3h
504+
it('should not trust arbitrary IPv4 for a short-prefix mapped subnet', function () {
505+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/8'])
506+
assert.strictEqual(trust('8.8.8.8'), false)
507+
assert.strictEqual(trust('203.0.113.9'), false)
508+
assert.strictEqual(trust('0.0.0.0'), false)
509+
assert.strictEqual(trust('10.0.0.1'), false)
510+
})
511+
512+
it('should trust the intended block when written as a /104 mapped subnet', function () {
513+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/104'])
514+
assert.strictEqual(trust('10.0.0.1'), true)
515+
assert.strictEqual(trust('8.8.8.8'), false)
516+
})
517+
518+
it('should behave like the plain IPv4 subnet', function () {
519+
var trust = proxyaddr.compile(['10.0.0.0/8'])
520+
assert.strictEqual(trust('10.0.0.1'), true)
521+
assert.strictEqual(trust('8.8.8.8'), false)
522+
})
523+
524+
it('should treat a /96 mapped subnet as all IPv4', function () {
525+
var trust = proxyaddr.compile(['::ffff:0.0.0.0/96'])
526+
assert.strictEqual(trust('10.0.0.1'), true)
527+
assert.strictEqual(trust('8.8.8.8'), true)
528+
})
529+
530+
it('should reject a mapped subnet whose prefix is below 96', function () {
531+
var trust = proxyaddr.compile(['::ffff:0.0.0.0/95'])
532+
assert.strictEqual(trust('8.8.8.8'), false)
533+
})
534+
535+
it('should honor a /97 mapped subnet as a proper subset of IPv4', function () {
536+
var trust = proxyaddr.compile(['::ffff:0.0.0.0/97'])
537+
assert.strictEqual(trust('8.8.8.8'), true)
538+
assert.strictEqual(trust('120.0.0.1'), true)
539+
assert.strictEqual(trust('200.0.0.1'), false)
540+
})
541+
542+
it('should not trust IPv4 for an IPv6 subnet with zero leading bits', function () {
543+
var trust = proxyaddr.compile(['::/1'])
544+
assert.strictEqual(trust('8.8.8.8'), false)
545+
assert.strictEqual(trust('::ffff:8.8.8.8'), false)
546+
assert.strictEqual(trust('::ffff:10.0.0.1'), false)
547+
})
548+
549+
it('should leave ordinary IPv6 subnets unaffected', function () {
550+
var trust = proxyaddr.compile(['fc00::/7'])
551+
assert.strictEqual(trust('8.8.8.8'), false)
552+
assert.strictEqual(trust('fc00::1'), true)
553+
})
554+
555+
it('should leave the built-in uniquelocal name unaffected', function () {
556+
var trust = proxyaddr.compile('uniquelocal')
557+
assert.strictEqual(trust('10.0.0.1'), true)
558+
assert.strictEqual(trust('8.8.8.8'), false)
559+
})
560+
561+
it('should neutralize a short-prefix mapped subnet in a multi-subnet list', function () {
562+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/8', '10.0.0.0/8'])
563+
assert.strictEqual(trust('10.0.0.1'), true)
564+
assert.strictEqual(trust('8.8.8.8'), false)
565+
})
566+
567+
it('should not believe X-Forwarded-For from an untrusted socket peer', function () {
568+
var req = createReq('127.0.0.1', {
569+
'x-forwarded-for': '9.9.9.9'
570+
})
571+
assert.strictEqual(proxyaddr(req, ['::ffff:10.0.0.0/8']), '127.0.0.1')
572+
})
573+
574+
it('should not trust an IPv4-mapped candidate for a short-prefix mapped subnet', function () {
575+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/8'])
576+
assert.strictEqual(trust('::ffff:8.8.8.8'), false)
577+
assert.strictEqual(trust('::ffff:1.2.3.4'), false)
578+
assert.strictEqual(trust('::ffff:10.0.0.1'), false)
579+
})
580+
581+
it('should trust an IPv4-mapped candidate for a /104 mapped subnet', function () {
582+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/104'])
583+
assert.strictEqual(trust('::ffff:10.0.0.1'), true)
584+
assert.strictEqual(trust('::ffff:8.8.8.8'), false)
585+
})
586+
587+
it('should match a mapped candidate identically to its IPv4 form', function () {
588+
var trust = proxyaddr.compile(['10.0.0.0/8'])
589+
assert.strictEqual(trust('::ffff:10.0.0.1'), true)
590+
assert.strictEqual(trust('::ffff:8.8.8.8'), false)
591+
})
592+
593+
it('should not believe X-Forwarded-For from an untrusted IPv4-mapped socket peer', function () {
594+
var req = createReq('::ffff:8.8.8.8', {
595+
'x-forwarded-for': '9.9.9.9'
596+
})
597+
assert.strictEqual(proxyaddr(req, ['::ffff:10.0.0.0/8']), '::ffff:8.8.8.8')
598+
})
599+
600+
it('should not trust native IPv6 for a short-prefix mapped subnet', function () {
601+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/8'])
602+
assert.strictEqual(trust('::1'), false)
603+
assert.strictEqual(trust('::abcd'), false)
604+
var trust95 = proxyaddr.compile(['::ffff:0.0.0.0/95'])
605+
assert.strictEqual(trust95('::fffe:0:0'), false)
606+
})
607+
608+
it('should not trust native IPv6 for a valid mapped subnet', function () {
609+
var trust = proxyaddr.compile(['::ffff:10.0.0.0/104'])
610+
assert.strictEqual(trust('::1'), false)
611+
assert.strictEqual(trust('10.0.0.1'), true)
612+
})
613+
614+
it('should still trust native IPv6 within an ordinary IPv6 subnet', function () {
615+
var trust = proxyaddr.compile(['2001:db8::/32'])
616+
assert.strictEqual(trust('2001:db8::1'), true)
617+
assert.strictEqual(trust('::1'), false)
618+
})
619+
})
620+
502621
function createReq (socketAddr, headers) {
503622
return {
504623
connection: {

0 commit comments

Comments
 (0)