Skip to content

Fix contains() throwing on cross-family addresses (#62) - #63

Open
youdie006 wants to merge 1 commit into
rs:masterfrom
youdie006:fix/62-contains-cross-family
Open

Fix contains() throwing on cross-family addresses (#62)#63
youdie006 wants to merge 1 commit into
rs:masterfrom
youdie006:fix/62-contains-cross-family

Conversation

@youdie006

Copy link
Copy Markdown

Fixes #62.

Problem

Netmask.contains() is a boolean predicate, but it throws a low-level parser error when the argument is a validly-formatted address of the other family than the range:

new Netmask("0.0.0.0/8").contains("::ffff:127.0.0.1")  // Error: empty octet
new Netmask("fc00::/7").contains("10.0.0.1")           // Error: Invalid IPv6: expected 8 groups, got 1

A predicate that throws on a valid other-family address crashes callers that use contains() in allowlist / SSRF guards, where the expected answer is simply false. Both throws are reported in #62 (v4 range + v6 address, and the reverse).

Root cause

The unified contains() delegates a plain IP string straight to this._impl.contains(...). When the range and the address are different families, the impl's parser (ip2long/parseNum for v4, parseIPv6Pure for v6) throws instead of returning a boolean.

Fix

Before delegating, detect the argument's family with a plain string ':' check -- the same signal the constructor already uses to pick the impl -- and short-circuit to return false when it differs from the range's family. This deliberately avoids the security-sensitive parsing internals (this library has parsing CVEs); it is a pure string check, no address parsing on the reject path. Returning false matches the reporter's stated expectation.

Note: normalizing IPv4-mapped IPv6 addresses (::ffff:x) to their IPv4 form is out of scope here; for the issue's own example it would still be false, since 0.0.0.0/8 does not cover 127.0.0.1.

Tests

Added tests/contains-cross-family.ts. Red/green verified: with the fix reverted the two cross-family cases throw the exact errors from the issue; with the fix all pass. Same-family containment (v4-in-v4, v6-in-v6) is asserted to still work. Full suite 182 passing, tsc --noEmit clean.


AI-assisted: this change was prepared with Claude Code (red/green tested).

Netmask.contains() is a boolean predicate but threw a low-level parser error when
the argument is a validly-formatted address of the other family than the range
(e.g. new Netmask('0.0.0.0/8').contains('::ffff:127.0.0.1') -> 'empty octet', and
the reverse). A predicate that throws on a valid other-family address crashes
callers using contains() in allowlist/SSRF guards, where the expected answer is
false. The unified contains() delegated a plain IP string straight to
this._impl.contains(), and the impl parser throws on an other-family address.

Detect the argument's family with a plain string ':' check (the same signal the
constructor uses) and short-circuit to false on family mismatch, avoiding the
security-sensitive parsing internals entirely.

Fixes rs#62
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.contains() throws "empty octet" for IPv4-mapped IPv6 addresses

1 participant