fix(security): close SSRF bypass via IPv6, CGNAT, and IPv4-mapped ranges in config set - #2305
Conversation
…ges in config set Replace the incomplete prefix-based isPrivateIp() in sandbox-config.ts with a comprehensive CIDR-based implementation aligned with the canonical ssrf.ts in the plugin. The previous check missed: - 100.64.0.0/10 (RFC 6598 CGNAT shared address space) - 198.18.0.0/15 (RFC 2544 benchmark testing) - fc00::/7 (IPv6 unique local, RFC 4193) - fe80::/10 (IPv6 link-local, RFC 4291) - ff00::/8 (IPv6 multicast, RFC 4291) - ::ffff:x.x.x.x (IPv4-mapped IPv6 addresses) This allowed private/internal addresses to be injected into sandbox config via 'nemoclaw config set', potentially exposing internal services (cloud metadata endpoints, adjacent VPC workloads) to the sandbox agent. The fix uses proper CIDR matching with IPv4/IPv6 byte parsing, handles URL.hostname bracket stripping for IPv6, and detects IPv4-mapped IPv6 addresses to check embedded IPv4 against IPv4 ranges. Adds 13 new test cases to test/config-set.test.ts covering all previously missing ranges plus boundary/edge cases. Signed-off-by: Siddhartha Singh <siddharthagithub0007@gmail.com>
📝 WalkthroughWalkthroughThe URL validation logic for detecting private/internal hosts is enhanced from simple prefix matching to comprehensive RFC-based IP address range validation. The implementation now parses IPv4 and IPv6 addresses into byte arrays, handles IPv6 compression and IPv4-mapped formats, and performs CIDR bitmask comparisons against an expanded set of reserved and private network ranges. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/lib/sandbox-config.ts (1)
223-226: Extract this classifier instead of copying it.This reintroduces a second copy of the IP parser/CIDR table even though
nemoclaw/src/blueprint/ssrf.ts:104-119is called out as canonical. Given this PR is fixing drift between the two paths, sharing one helper would make the next range update much less likely to miss one side.Also applies to: 287-357
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/sandbox-config.ts` around lines 223 - 226, Duplicate IP classifier logic should be extracted into a single shared helper (e.g., export a function like isPrivateOrReservedIp and the CIDR table constant PRIVATE_RESERVED_CIDRS) and the copies in sandbox-config (the block around lines 223-226 and 287-357) replaced with imports from that new module; update both callers (including the canonical consumer in nemoclaw/src/blueprint/ssrf.ts) to call the shared isPrivateOrReservedIp so future CIDR range updates are made in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/sandbox-config.ts`:
- Around line 338-341: The current check only allows the literal "localhost" and
misses special-use names like "localhost." and "*.localhost", so update the
early-return logic (the block that checks hostname and the derived addr
variable) to treat any loopback name that is exactly "localhost", "localhost."
or any subdomain ending with ".localhost" as local — i.e., perform the check
after stripping IPv6 brackets on addr and return true when addr equals
"localhost" (with or without trailing dot) or when addr ends with ".localhost".
---
Nitpick comments:
In `@src/lib/sandbox-config.ts`:
- Around line 223-226: Duplicate IP classifier logic should be extracted into a
single shared helper (e.g., export a function like isPrivateOrReservedIp and the
CIDR table constant PRIVATE_RESERVED_CIDRS) and the copies in sandbox-config
(the block around lines 223-226 and 287-357) replaced with imports from that new
module; update both callers (including the canonical consumer in
nemoclaw/src/blueprint/ssrf.ts) to call the shared isPrivateOrReservedIp so
future CIDR range updates are made in one place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d4851812-ab36-48a0-a2a8-0960112a88ce
📒 Files selected for processing (2)
src/lib/sandbox-config.tstest/config-set.test.ts
| if (hostname === "localhost") return true; | ||
|
|
||
| // URL.hostname wraps IPv6 in brackets — strip them for matching | ||
| const addr = hostname.replace(/^\[|\]$/g, ""); |
There was a problem hiding this comment.
Block .localhost names, not just literal localhost.
localhost. and any *.localhost hostname are special-use loopback names. The exact-string check here leaves an SSRF bypass even after the CIDR expansion.
🔒 Suggested fix
function isPrivateIp(hostname: string): boolean {
- if (hostname === "localhost") return true;
-
- // URL.hostname wraps IPv6 in brackets — strip them for matching
- const addr = hostname.replace(/^\[|\]$/g, "");
+ // URL.hostname may include IPv6 brackets and a trailing dot on FQDNs.
+ const addr = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
+ if (addr === "localhost" || addr.endsWith(".localhost")) return true;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (hostname === "localhost") return true; | |
| // URL.hostname wraps IPv6 in brackets — strip them for matching | |
| const addr = hostname.replace(/^\[|\]$/g, ""); | |
| // URL.hostname may include IPv6 brackets and a trailing dot on FQDNs. | |
| const addr = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase(); | |
| if (addr === "localhost" || addr.endsWith(".localhost")) return true; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/sandbox-config.ts` around lines 338 - 341, The current check only
allows the literal "localhost" and misses special-use names like "localhost."
and "*.localhost", so update the early-return logic (the block that checks
hostname and the derived addr variable) to treat any loopback name that is
exactly "localhost", "localhost." or any subdomain ending with ".localhost" as
local — i.e., perform the check after stripping IPv6 brackets on addr and return
true when addr equals "localhost" (with or without trailing dot) or when addr
ends with ".localhost".
|
✨ Thanks for submitting this issue that identifies a bug with the SSRF validation and proposes a fix. |
1 similar comment
|
✨ Thanks for submitting this issue that identifies a bug with the SSRF validation and proposes a fix. |
NVIDIA#2324) <!-- markdownlint-disable MD041 --> ## Summary Replace the weak prefix-based isPrivateIp in src/lib/sandbox-config.ts with a node:net BlockList built from a new canonical source under nemoclaw-blueprint/private-networks.yaml. The plugin's nemoclaw/src/blueprint/ssrf.ts now loads the same data, so the drift that allowed NVIDIA#2300 to exist cannot recur. ## Related Issue Fixes NVIDIA#2300 Supersedes and closes NVIDIA#2305 ## Changes * Coverage added: CGNAT, IETF protocol assignments (incl. DS-Lite), IPv4 documentation ranges, IPv4 multicast and reserved-for-future-use (with 255.255.255.255 limited broadcast), and the IPv6 translation prefixes NAT64 well-known, NAT64 local-use, Teredo, and 6to4. IPv4-mapped IPv6 (::ffff:x.x.x.x) is handled by BlockList's cross-family auto-match; no custom extraction needed. * Adds a 'names' section to the YAML for reserved private/internal name-level matches — localhost (RFC 6761), local (RFC 6762 mDNS), and internal (ICANN-reserved 2024 private-use TLD). Matching is case-insensitive and trailing-dot-normalised, covering the CodeRabbit finding on NVIDIA#2305 about *.localhost and FQDN-form variants. * Every YAML entry requires a non-empty purpose field so blocks ship with a human-reviewable rationale rather than a bare CIDR or bare name. * The new test/ssrf-parity.test.ts guards against drift: schema checks plus per-CIDR boundary vectors (start, end, two middles, one below start, one above end) asserting CLI and plugin isPrivateIp agree. The plugin-side private-networks.test.ts covers path resolution and schema-validation error branches. ## Type of Change - [X] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification <!-- Check each item you ran and confirmed. Leave unchecked items you skipped. --> - [X] `npx prek run --all-files` passes - [X] `npm test` passes - [X] Tests added or updated for new or changed behavior - [X] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ## AI Disclosure <!-- If an AI agent authored or co-authored this PR, check the box and name the tool. Remove this section for fully human-authored PRs. --> - [X] AI-assisted — tool: Claude Code<!-- e.g., Claude Code, Cursor, GitHub Copilot --> --- <!-- DCO sign-off required by CI. Run: git config user.name && git config user.email --> Signed-off-by: Tinson Lai <tinsonl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a canonical blueprint of private/reserved IPv4/IPv6 ranges and reserved hostnames used for SSRF blocking; every entry includes a documented human-readable purpose. * **Refactor** * Centralized endpoint validation to use the new private-network checks with normalized hostname handling (IPv6 bracket/trailing-dot trimming, case-insensitive matching) and memoized lookups. * Config URL validation now trims whitespace and accepts mixed-case schemes before checking for private hosts. * **Tests** * Added extensive unit and parity tests for schema validation, caching/reset behavior, CIDR boundary vectors, name matching, and input normalization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> Co-authored-by: Aaron Erickson 🦞 <aerickson@nvidia.com>
NVIDIA#2324) <!-- markdownlint-disable MD041 --> ## Summary Replace the weak prefix-based isPrivateIp in src/lib/sandbox-config.ts with a node:net BlockList built from a new canonical source under nemoclaw-blueprint/private-networks.yaml. The plugin's nemoclaw/src/blueprint/ssrf.ts now loads the same data, so the drift that allowed NVIDIA#2300 to exist cannot recur. ## Related Issue Fixes NVIDIA#2300 Supersedes and closes NVIDIA#2305 ## Changes * Coverage added: CGNAT, IETF protocol assignments (incl. DS-Lite), IPv4 documentation ranges, IPv4 multicast and reserved-for-future-use (with 255.255.255.255 limited broadcast), and the IPv6 translation prefixes NAT64 well-known, NAT64 local-use, Teredo, and 6to4. IPv4-mapped IPv6 (::ffff:x.x.x.x) is handled by BlockList's cross-family auto-match; no custom extraction needed. * Adds a 'names' section to the YAML for reserved private/internal name-level matches — localhost (RFC 6761), local (RFC 6762 mDNS), and internal (ICANN-reserved 2024 private-use TLD). Matching is case-insensitive and trailing-dot-normalised, covering the CodeRabbit finding on NVIDIA#2305 about *.localhost and FQDN-form variants. * Every YAML entry requires a non-empty purpose field so blocks ship with a human-reviewable rationale rather than a bare CIDR or bare name. * The new test/ssrf-parity.test.ts guards against drift: schema checks plus per-CIDR boundary vectors (start, end, two middles, one below start, one above end) asserting CLI and plugin isPrivateIp agree. The plugin-side private-networks.test.ts covers path resolution and schema-validation error branches. ## Type of Change - [X] Code change (feature, bug fix, or refactor) - [ ] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Verification <!-- Check each item you ran and confirmed. Leave unchecked items you skipped. --> - [X] `npx prek run --all-files` passes - [X] `npm test` passes - [X] Tests added or updated for new or changed behavior - [X] No secrets, API keys, or credentials committed - [ ] Docs updated for user-facing behavior changes - [ ] `make docs` builds without warnings (doc changes only) - [ ] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) (doc changes only) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ## AI Disclosure <!-- If an AI agent authored or co-authored this PR, check the box and name the tool. Remove this section for fully human-authored PRs. --> - [X] AI-assisted — tool: Claude Code<!-- e.g., Claude Code, Cursor, GitHub Copilot --> --- <!-- DCO sign-off required by CI. Run: git config user.name && git config user.email --> Signed-off-by: Tinson Lai <tinsonl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a canonical blueprint of private/reserved IPv4/IPv6 ranges and reserved hostnames used for SSRF blocking; every entry includes a documented human-readable purpose. * **Refactor** * Centralized endpoint validation to use the new private-network checks with normalized hostname handling (IPv6 bracket/trailing-dot trimming, case-insensitive matching) and memoized lookups. * Config URL validation now trims whitespace and accepts mixed-case schemes before checking for private hosts. * **Tests** * Added extensive unit and parity tests for schema validation, caching/reset behavior, CIDR boundary vectors, name matching, and input normalization. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Tinson Lai <tinsonl@nvidia.com> Co-authored-by: Aaron Erickson 🦞 <aerickson@nvidia.com>
Summary
Closes the SSRF validation gap in
nemoclaw config setby replacing the incomplete prefix-basedisPrivateIp()insandbox-config.tswith a comprehensive CIDR-based implementation that matches the coverage of the canonicalssrf.tsin the plugin.Related Issue
Changes
isPrivateIp()with a proper CIDR-based implementation usingnode:netfor IPv4/IPv6 detection100.64.0.0/10) — previously allowed through198.18.0.0/15) — previously allowed throughfc00::/7), link-local (fe80::/10), multicast (ff00::/8)::ffff:x.x.x.x) — extracts embedded IPv4 and checks against IPv4 rangesURL.hostnamewraps IPv6 in[], now properly stripped before matchingtest/config-set.test.tscovering all previously missing ranges plus boundary/edge casesType of Change
Verification
npx prek run --all-filespassesnpm testpassesmake docsbuilds without warnings (doc changes only)Signed-off-by: Siddhartha Singh siddharthagithub0007@gmail.com
Summary by CodeRabbit