You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by "GabrielDrapor (via GitHub)" <gi...@apache.org> on 2026/08/22 13:54:43 UTC
[PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
GabrielDrapor opened a new pull request, #3502:
URL: https://github.com/apache/maka/pull/3502
Follow-up to @Astro-Han's [review note on #2653](https://github.com/apache/maka/pull/2653#issuecomment-latest) about the `isLoopbackHost`/`isPrivateRangeHost` IPv6 blind spot, now that the code lives on main via #2920's squash.
## The gap
`isPrivateRangeHost` matched hostname **spellings** — and WHATWG URL parsing canonicalizes them before the gate ever sees one: `[::ffff:127.0.0.1]` arrives as `[::ffff:7f00:1]`, which matched nothing, so every IPv4-mapped spelling of a loopback or private destination passed the remote-provenance https check (fail **open**, exactly the direction the note called out). The `'fe8'` prefix check also covered only a quarter of fe80::/10 — `fe9x`/`feax`/`febx` link-local spellings slipped through.
## The fix — with the opposite-defaults treatment from the review
The literal now parses to its 16 bytes (a minimal RFC 4291 text parser, no resolver) and classifies structurally:
- IPv4-mapped (`::ffff/96`) and NAT64 (`64:ff9b::/96`) embeddings classify by their embedded IPv4 — **including 127/8, which lands on the PRIVATE side of the gate on purpose**;
- `isLoopbackHost` deliberately does **not** learn the mapped spellings: its unknown-spelling default stays "not loopback" so cleartext stays refused (fail closed), while this gate's default stays "private" so the request is blocked (also fail closed) — the two defaults point in opposite directions, as the note prescribed;
- an unparseable bracketed literal is treated as private;
- fc00::/7 and the **full** fe80::/10 are matched on bytes;
- global IPv6 stays reachable (`[2606:4700::1]`, mapped `8.8.8.8`) — real OAuth endpoints on IPv6 keep working.
(The #2920 loopback listener is unaffected, as the note observed — it pins `Host` to the exact `127.0.0.1:<port>` literal.)
## Regressions
- `packages/core`: unit coverage across spellings of one address (raw, uppercase, uncompressed, canonical), the full fe80::/10 range, NAT64, `[::1]`/loopback asymmetry, global reachability, and garbage-in-brackets failing closed.
- `packages/mcp` transport-security: end-to-end — mapped private/loopback https refused under remote provenance; public IPv6 allowed; cleartext to the mapped loopback still refused by the strict loopback predicate.
Suites: core 608, mcp 171 — green; biome + ASF header audit clean.
Co-Authored-By: Claude <no...@anthropic.com>
https://claude.ai/code/session_01TMwYxgNEbz2RFmuK6AXGcj
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on PR #3502:
URL: https://github.com/apache/maka/pull/3502#issuecomment-5383989161
Second review pass on `f0707b1e89338ca3bc5f4999cd04031f001c25bf`. We ran two independent lines on this one plus a hands-on reachability check, because a private-range gate fails silently — a wrong verdict produces no error and no log, just a request that should have been blocked and wasn't.
The parsing rewrite itself holds up. Classifying by the 16 parsed bytes instead of by spelling is the right move, and it is a genuine simplification: `startsWith('fc' | 'fd' | 'fe8')` goes away and there is now one authority on what an address *is*, rather than a growing list of textual forms. Mapped, NAT64, compat and SIIT embeddings all classify by their embedded IPv4; an unparseable bracketed literal falls to private; `isLoopbackHost` deliberately stays strict-by-spelling so the two defaults fail closed in opposite directions. We checked `fe9a::` / `febf::` (now private, previously missed by the `fe8` prefix), `[::ffff:7f00:1]` (the form WHATWG canonicalizes `[::ffff:127.0.0.1]` into), `::/96`, SIIT and `64:ff9b:1::/48` — all now behave.
**One finding.**
## [P1] The bare IPv4 unspecified address `0.0.0.0` is still classified as public
`isPrivateRangeHost` sends IPv4 literals down a separate path from the IPv6 one:
- `packages/core/src/mcp.ts:100-102` — the v4 branch calls `isPrivateIpv4(Number(v4[1]), Number(v4[2]))`, passing **only the first two octets**.
- `packages/core/src/mcp.ts:110-116` — `isPrivateIpv4` has no case for `a === 0`, so `isPrivateIpv4(0, 0) === false`.
- `packages/core/src/mcp.ts:208-212` — the embedded path already handles exactly this address, with this rationale written into the comment:
> *The unspecified embedded address (0.0.0.0) under ANY embedding: connecting to it reaches the LOCAL machine on common stacks (`::ffff:0:0` verifiably reaches a 127.0.0.1-bound listener) — private, fail closed.*
That reasoning is correct, and it is already applied to every IPv6 embedding of the address. It just never reaches the bare v4 literal, which does not pass through `classifyIpv6` at all.
Nothing downstream catches it: `isLoopbackHost` only recognizes `localhost`, `[::1]` and `127.x.x.x`, and the https branch in `transport-security.ts:59-75` allows anything that is neither loopback nor private.
**Reproduction** (Node 22, listener bound to `127.0.0.1`, placeholder port):
```
isPrivateRangeHost('0.0.0.0') -> false
isLoopbackHost('0.0.0.0') -> false
new URL('https://0/').hostname -> '0.0.0.0' // also 'https://0x0/'
fetch('http://0.0.0.0:<port>/') -> 200 local-ok // reaches the 127.0.0.1 listener
fetch('http://0:<port>/') -> 200 local-ok
```
For contrast, on this same head `https://[::]/` and `https://[::ffff:0:0]/` are both correctly refused. Same machine, same reachability, opposite verdicts depending on how the address is spelled — which is the exact class of inconsistency this PR set out to remove.
The `https://0/` form matters: WHATWG canonicalizes it to `0.0.0.0`, so a redirect target or an OAuth metadata document only needs to say `https://0/` to get through the gate.
**Suggested fix:** have `isPrivateIpv4` see all four octets and treat **only** `0.0.0.0` as private. We'd suggest *not* pulling in all of `0.0.0.0/8` — we checked `0.0.0.1` and it does not reach a local listener, so widening the private set there would cost coverage without buying anything. Worth adding `https://0.0.0.0/` and `https://0/` to the transport-security tests.
## CI
Separately from the review: the run on this head is red at `Check formatting` (the two `mcp.ts` layout sites noted in our earlier comment). Everything after that step is `skipped`, so the two new test files in this PR have still never executed in CI.
No P0/P2/P3 findings. Not approving yet — the P1 above, and no terminal-green required check on this head.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "GabrielDrapor (via GitHub)" <gi...@apache.org>.
GabrielDrapor commented on PR #3502:
URL: https://github.com/apache/maka/pull/3502#issuecomment-5384069007
Fixed at head `4e0084303` — exactly as suggested: `isPrivateIpv4` now sees all four octets and treats **only** the exact `0.0.0.0` as private (your `0.0.0.1` probe kept `0.0.0.0/8` out). The embedded checks route through the same predicate now, replacing their separate unspecified-address special case, so the rationale lives in one place for both the plain-v4 branch and every embedding.
Regressions: core cases for `0.0.0.0` / `https://0/` round-trip / `0.0.0.1` staying global, plus transport-security remote-provenance cases for `https://0.0.0.0:8443/` and `https://0/`. Also reformatted two over-long lines from the previous commit that were failing `format:check` (caught it locally this time). core 612, mcp 171 — green.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #3502:
URL: https://github.com/apache/maka/pull/3502#discussion_r3836261926
##########
packages/core/src/mcp.ts:
##########
@@ -81,25 +81,107 @@ export function isLoopbackHost(hostname: string): boolean {
/** Private-range and link-local IP LITERALS (RFC 1918, RFC 3927/4291,
* CGNAT). Hostname-based checks are deliberately out of scope: they would
* need a resolve here and could still re-resolve differently at request
- * time — callers treat privately-RESOLVING names as accepted risk. */
+ * time — callers treat privately-RESOLVING names as accepted risk.
+ *
+ * IPv6 literals are classified by PARSED BYTES, not spelling: WHATWG URL
+ * parsing canonicalizes hostnames (`[::ffff:127.0.0.1]` arrives here as
+ * `[::ffff:7f00:1]`), so any spelling-based match is one canonicalization
+ * away from missing an address it means to cover. IPv4-mapped (::ffff/96)
+ * and NAT64 (64:ff9b::/96) embeddings classify by their embedded IPv4 —
+ * INCLUDING 127/8, which `isLoopbackHost` deliberately does not learn:
+ * there, an unrecognized spelling must stay "not loopback" so cleartext is
+ * refused (fail closed); here it must stay "private" so the SSRF gate
+ * blocks it (also fail closed) — the two defaults point in opposite
+ * directions on purpose. A bracketed literal that does not parse at all is
+ * therefore treated as private. */
export function isPrivateRangeHost(hostname: string): boolean {
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(hostname);
if (v4) {
- const [a, b] = [Number(v4[1]), Number(v4[2])];
- if (a === 10) return true;
- if (a === 172 && b >= 16 && b <= 31) return true;
- if (a === 192 && b === 168) return true;
- if (a === 169 && b === 254) return true;
- if (a === 100 && b >= 64 && b <= 127) return true;
- return false;
+ return isPrivateIpv4(Number(v4[1]), Number(v4[2]));
}
- if (hostname.startsWith('[')) {
- const inner = hostname.slice(1, -1).toLowerCase();
- return inner.startsWith('fc') || inner.startsWith('fd') || inner.startsWith('fe8');
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
+ const bytes = parseIpv6(hostname.slice(1, -1));
+ if (!bytes) return true;
+ return classifyIpv6(bytes) === 'private';
}
return false;
}
+function isPrivateIpv4(a: number, b: number): boolean {
+ if (a === 10) return true;
+ if (a === 172 && b >= 16 && b <= 31) return true;
+ if (a === 192 && b === 168) return true;
+ if (a === 169 && b === 254) return true;
+ if (a === 100 && b >= 64 && b <= 127) return true;
+ return false;
+}
+
+/** Minimal RFC 4291 text-representation parser: enough to turn any spelling
+ * of an address into its 16 bytes so the classifier never depends on how a
+ * caller (or the URL canonicalizer) chose to write it. Returns undefined
+ * for anything that is not a well-formed address. */
+function parseIpv6(literal: string): Uint8Array | undefined {
+ const zone = literal.indexOf('%');
+ const text = (zone === -1 ? literal : literal.slice(0, zone)).toLowerCase();
+ const gap = text.indexOf('::');
+ if (gap !== -1 && text.indexOf('::', gap + 1) !== -1) return undefined;
+ const parseGroups = (part: string): number[] | undefined => {
+ if (part === '') return [];
+ const out: number[] = [];
+ for (const piece of part.split(':')) {
+ if (piece.includes('.')) {
+ const dotted = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(piece);
+ if (!dotted) return undefined;
+ const octets = dotted.slice(1).map(Number);
+ if (octets.some((octet) => octet > 255)) return undefined;
+ out.push(((octets[0] ?? 0) << 8) | (octets[1] ?? 0), ((octets[2] ?? 0) << 8) | (octets[3] ?? 0));
+ } else {
+ if (!/^[0-9a-f]{1,4}$/u.test(piece)) return undefined;
+ out.push(Number.parseInt(piece, 16));
+ }
+ }
+ return out;
+ };
+ let groups: number[] | undefined;
+ if (gap === -1) {
+ groups = parseGroups(text);
+ if (!groups || groups.length !== 8) return undefined;
+ } else {
+ const head = parseGroups(text.slice(0, gap));
+ const tail = parseGroups(text.slice(gap + 2));
+ if (!head || !tail || head.length + tail.length > 7) return undefined;
+ groups = [...head, ...new Array<number>(8 - head.length - tail.length).fill(0), ...tail];
+ }
+ const bytes = new Uint8Array(16);
+ groups.forEach((group, index) => {
+ bytes[2 * index] = group >> 8;
+ bytes[2 * index + 1] = group & 0xff;
+ });
+ return bytes;
+}
+
+function classifyIpv6(bytes: Uint8Array): 'loopback' | 'private' | 'global' {
Review Comment:
**[P2] IPv4-compatible `::/96` is not classified by its embedded IPv4.**
`classifyIpv6` recognizes two ways of embedding an IPv4 address — `::ffff:/96` and `64:ff9b::/96` — and hands both to `isPrivateIpv4`. It does not recognize the third, `::/96`, so those addresses reach the final `return 'global'`.
Reproduction, using the URL parser this gate sits behind:
```js
new URL('https://[::192.168.1.1]/').hostname // '[::c0a8:101]'
new URL('https://[::127.0.0.1]/').hostname // '[::7f00:1]'
```
Trace `[::7f00:1]` through the function: bytes 0–11 are zero so `mapped` is false (byte 10 is not `0xff`) and `nat64` is false (byte 1 is not `0x64`); the loopback check requires bytes 0–14 to be zero and byte 12 is `0x7f`; `fc00::/7` and `fe80::/10` both test byte 0, which is zero. Result: `global`. `isPrivateRangeHost('[::7f00:1]')` returns `false`, so the remote-HTTPS branch in `transport-security.ts:59-75` does not reject it, and a remote server's metadata or a redirect can still hand back a target spelled this way.
**On severity.** We tried to connect to `::127.0.0.1` on macOS and it times out rather than reaching the loopback listener, so on a current stack this is unlikely to be an exploitable path to a live service — IPv4-compatible addresses are deprecated by RFC 4291 and most stacks no longer translate them. That is why this is a P2 and not a P1. But it should still be closed, for a reason that does not depend on reachability: this PR's whole premise is that classification happens on parsed bytes rather than on how an address is written, and `::/96` is the one byte-level IPv4 embedding the classifier does not know about. Leaving it means the gate's correctness rests on an assumption about the *other* end's network stack, which is exactly the kind of assumption this change set out to remove.
Smallest fix: treat `::/96` alongside the other two — bytes 0–11 all zero, excluding `::` and `::1` which the surrounding checks already own — and reuse the same `isPrivateIpv4` / `127.` logic. Worth a regression case that comes in through the remote-provenance path rather than only calling the classifier directly, since that is where a real one would arrive.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on PR #3502:
URL: https://github.com/apache/maka/pull/3502#issuecomment-5383964484
Heads up on CI: the workflow run for this PR was sitting in `action_required` (fork PRs need a maintainer to approve the run), so it had never actually executed. I approved it — it has now run on `f0707b1e89338ca3bc5f4999cd04031f001c25bf` and came back **red**.
**Failing step:** `Check formatting` (Biome), one file — `packages/core/src/mcp.ts`, two sites:
- around L145, the `out.push(...)` for the dotted-quad octets wants to be split across lines
- around L187, the six-way `bytes[0] === 0 && ...` NAT64 `64:ff9b::/96` check wants to be broken up
`npm run format` (or `biome format --write`) should settle both — it's purely layout, no logic involved.
**Worth flagging explicitly:** `Check formatting` runs early in the job, so everything downstream was reported as `skipped`, not passed — `Build`, `Typecheck`, `Run affected standard workspace tests`, `Run Runtime Host tests`, and the rest. The two new test files in this PR (`mcp-host-classification.test.ts` and the `transport-security.test.ts` additions) have therefore **never run in CI**. Red-at-formatting is not a signal about the classification logic in either direction.
On the review: this is a private-range SSRF gate, so we're giving it full adversarial treatment (two independent lines plus a mechanical fact check) regardless of diff size — a wrong private/public verdict here fails silently, with no error and no log, which is exactly the failure mode worth spending review budget on. Conclusions will follow in a separate comment; nothing blocking from us yet beyond the formatting fix.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "GabrielDrapor (via GitHub)" <gi...@apache.org>.
GabrielDrapor commented on PR #3502:
URL: https://github.com/apache/maka/pull/3502#issuecomment-5381269419
Follow-up at head `f0707b1e8`, from an independent adversarial pass (Codex) I ran against the previous head — three findings, all fixed:
- **[P1] `[::ffff:0:0]` classified global.** A runtime probe confirmed connecting to `::ffff:0.0.0.0` reaches a 127.0.0.1-bound listener. The embedded-`0.0.0.0` → private rule now applies under every embedding, not only IPv4-compatible.
- **[P1] Translation ranges unclassified.** RFC 8215's local-use NAT64 space `64:ff9b:1::/48` now fails closed as a whole (it is reserved for in-network translation, and deployments carve arbitrary RFC 6052 prefix lengths out of it, so the embedded IPv4's position is not recoverable); RFC 2765 SIIT `::ffff:0:0/96` classifies by its embedded IPv4 like the other exact /96 prefixes.
- **[P2] Parser over-acceptance.** Dotted IPv4 outside the low-order 32 bits (`[192.168.1.1::]`) and an empty zone id now fail parsing — and therefore fail closed as private — instead of parsing shifted. WHATWG URL rejects these spellings today, so the transport path was already covered; the exported classifier now honors its own documented contract regardless of caller.
Regressions for all three in both suites, including transport-security cases through the remote-provenance path (`[::ffff:0:0]:8443`, the /48 encoding of 192.168.1.1, SIIT). core 611, mcp 171 — green.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "GabrielDrapor (via GitHub)" <gi...@apache.org>.
GabrielDrapor commented on PR #3502:
URL: https://github.com/apache/maka/pull/3502#issuecomment-5381021184
Fixed at head `ba4f36d74` — `::/96` now classifies alongside the other two embeddings: bytes 0–11 all zero routes to the same embedded-IPv4 logic, with `::1` returned by the loopback check first (moved above the embedding checks so the compat path can never mis-claim it).
One deliberate extension beyond the smallest fix, flagged for push-back: the all-zero `::` (embedded `0.0.0.0`) is classified **private** rather than excluded — connecting to the unspecified address reaches the local machine on common stacks (Linux treats it as connect-to-loopback), and nothing else owned it: `isLoopbackHost` doesn't match `[::]`, so under the previous exclusion `https://[::]:8443/` would have passed the remote-provenance gate. Public v4-compatible embeddings (`::8.8.8.8`) stay global.
Regressions added in both places you suggested: core unit cases across the URL round-trip (`https://[::192.168.1.1]/` → `[::c0a8:101]`) and raw canonical forms, plus transport-security cases arriving through the remote-provenance path (`[::192.168.1.1]`, `[::127.0.0.1]:8443`, `[::]`). Suites: core 609, mcp 171 — green.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han merged PR #3502:
URL: https://github.com/apache/maka/pull/3502
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
Re: [PR] fix(core): classify IPv6 literals by parsed bytes in the private-range SSRF gate [maka]
Posted by "Astro-Han (via GitHub)" <gi...@apache.org>.
Astro-Han commented on code in PR #3502:
URL: https://github.com/apache/maka/pull/3502#discussion_r3837621770
##########
packages/core/src/mcp.ts:
##########
@@ -81,25 +81,142 @@ export function isLoopbackHost(hostname: string): boolean {
/** Private-range and link-local IP LITERALS (RFC 1918, RFC 3927/4291,
* CGNAT). Hostname-based checks are deliberately out of scope: they would
* need a resolve here and could still re-resolve differently at request
- * time — callers treat privately-RESOLVING names as accepted risk. */
+ * time — callers treat privately-RESOLVING names as accepted risk.
+ *
+ * IPv6 literals are classified by PARSED BYTES, not spelling: WHATWG URL
+ * parsing canonicalizes hostnames (`[::ffff:127.0.0.1]` arrives here as
+ * `[::ffff:7f00:1]`), so any spelling-based match is one canonicalization
+ * away from missing an address it means to cover. IPv4-mapped (::ffff/96)
+ * and NAT64 (64:ff9b::/96) embeddings classify by their embedded IPv4 —
+ * INCLUDING 127/8, which `isLoopbackHost` deliberately does not learn:
+ * there, an unrecognized spelling must stay "not loopback" so cleartext is
+ * refused (fail closed); here it must stay "private" so the SSRF gate
+ * blocks it (also fail closed) — the two defaults point in opposite
+ * directions on purpose. A bracketed literal that does not parse at all is
+ * therefore treated as private. */
export function isPrivateRangeHost(hostname: string): boolean {
const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/u.exec(hostname);
if (v4) {
- const [a, b] = [Number(v4[1]), Number(v4[2])];
- if (a === 10) return true;
- if (a === 172 && b >= 16 && b <= 31) return true;
- if (a === 192 && b === 168) return true;
- if (a === 169 && b === 254) return true;
- if (a === 100 && b >= 64 && b <= 127) return true;
- return false;
+ return isPrivateIpv4(Number(v4[1]), Number(v4[2]));
}
- if (hostname.startsWith('[')) {
- const inner = hostname.slice(1, -1).toLowerCase();
- return inner.startsWith('fc') || inner.startsWith('fd') || inner.startsWith('fe8');
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
+ const bytes = parseIpv6(hostname.slice(1, -1));
+ if (!bytes) return true;
+ return classifyIpv6(bytes) === 'private';
}
return false;
}
+function isPrivateIpv4(a: number, b: number): boolean {
+ if (a === 10) return true;
+ if (a === 172 && b >= 16 && b <= 31) return true;
+ if (a === 192 && b === 168) return true;
+ if (a === 169 && b === 254) return true;
+ if (a === 100 && b >= 64 && b <= 127) return true;
+ return false;
Review Comment:
`isPrivateIpv4(0, 0)` returns `false`, so the bare literal `0.0.0.0` is classified as public.
The embedded path at :208-212 already handles this exact address, with the rationale written into its own comment — *"connecting to it reaches the LOCAL machine on common stacks ... private, fail closed"*. That reasoning never reaches here, because the v4 branch at :100-102 doesn't go through `classifyIpv6` at all.
Verified on this head (Node 22, listener bound to 127.0.0.1):
```
new URL('https://0/').hostname -> '0.0.0.0'
fetch('http://0:<port>/') -> 200 // reaches the loopback listener
https://[::]/ -> refused (correct)
https://[::ffff:0:0]/ -> refused (correct)
```
Suggested fix: have `isPrivateIpv4` see all four octets and treat **only** `0.0.0.0` as private. We checked `0.0.0.1` and it does not reach a local listener, so pulling in all of `0.0.0.0/8` would widen the private set without buying anything.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org