net: enforce strict socket address formats in SocketAddress.parse - #64832
net: enforce strict socket address formats in SocketAddress.parse#64832araujogui wants to merge 5 commits into
Conversation
|
Review requested:
|
There was a problem hiding this comment.
This would make the port issue much simpler with this additional validation. If the string ends in /:(\d+)$/, then it can just be sliced off, and if the rest of the string gets parsed as a valid hostname by URLParse then the port can be obtained from the digit string with NumberParseInt.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #64832 +/- ##
==========================================
- Coverage 90.17% 90.16% -0.01%
==========================================
Files 771 772 +1
Lines 265470 265542 +72
Branches 50463 50487 +24
==========================================
+ Hits 239383 239426 +43
+ Misses 17055 17048 -7
- Partials 9032 9068 +36
🚀 New features to boost your workflow:
|
|
@Renegade334 maybe we should include |
|
I'm not yet convinced this is the best way to approach this (with the regex). Won't block for now but there's probably a more performant / reliable way. |
971d5b4 to
9d21a93
Compare
@jasnell I implemented a new strict socket address parser replacing Ada, it fixes #62906 and is faster. Benchmark results compared to main: |
9d21a93 to
75ae3d0
Compare
|
Benchmark GHA (net / net-socketaddress-parse): https://github.com/nodejs/node/actions/runs/33605572788 Results
Benchmark results:
|
Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
SocketAddress.parse() ran its input through the WHATWG URL host parser, which accepts far more than an address with an optional port. It took the legacy IPv4 notations behind CVE-2021-29922 and CVE-2021-29923, including octal, hexadecimal, integer and shorthand forms, and it dropped a port that matched the default for the assumed scheme, so '1.2.3.4:80' parsed as port 0. Replace it with a small parser that matches the documented grammar and nothing else. It only frames the input; the host is handed to uv_inet_pton, which rejects the legacy forms itself. IPv6 zone ids are deliberately outside the grammar. sin6_scope_id is not exposed on the JS object, so a scoped address could not round trip through its own JSON, and an interface name cannot be resolved without consulting the local interface table, which would make the result depend on the host it was parsed on. '%' stays in the IPv6 delimiter set so that it can never reach uv_ip6_addr(), which would otherwise resolve it. Assisted-by: Claude Opus 5 Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
75ae3d0 to
7fd8d95
Compare
The parse benchmark only measured inputs that parse successfully, so the rejection paths were unmeasured. Add a group covering the four ways the parser bails: a non-address host and a legacy hex form that only uv_inet_pton rejects, an out of range port, and an unterminated IPv6 address that never reaches uv_ip6_addr. Assisted-by: Claude Opus 5 Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
f702a10 to
45c3368
Compare
SocketAddress.parse()no longer builds ahttp://${input}URL and reads the hostname and port back out of the Ada WHATWG URL parser. The input is now split in C++ into host and port, and the host is validated byuv_inet_pton()— the same check theSocketAddressconstructor already used.The Ada URL parser is deliberately lenient about hosts, so
parse()accepted much that is not a socket address: legacy IPv4 forms (0177.0.0.1,0x7f.0.0.1,2130706433,127.1), URL syntax (user@1.2.3.4:80,1.2.3.4:80/foo), and input it rewrites first, such as embedded tabs and non-ASCII digits that IDNA maps to ASCII. Those legacy forms are the ambiguity behind CVE-2021-29923 and CVE-2021-29922. It also disagreed with the constructor, which rejects0177.0.0.1withERR_INVALID_ADDRESS.Separately,
SocketAddress.parse('1.2.3.4:80')returned port 0, because the URL parser drops a port equal to the scheme's default. It now returns 80, fixes #62906.The accepted grammar is documented in
doc/api/net.md. I left IPv6 zone ids out of it:sin6_scope_idisn't exposed on the JS object, so a scoped address can't round trip through its own JSON, and resolving an interface name would mean the result depends on which machine did the parsing.