Skip to content

fix(security): share private-network block list between CLI and plugin#2324

Open
laitingsheng wants to merge 9 commits intomainfrom
fix/2300-ssrf-blocklist
Open

fix(security): share private-network block list between CLI and plugin#2324
laitingsheng wants to merge 9 commits intomainfrom
fix/2300-ssrf-blocklist

Conversation

@laitingsheng
Copy link
Copy Markdown
Contributor

@laitingsheng laitingsheng commented Apr 23, 2026

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 #2300 to exist cannot recur.

Related Issue

Fixes #2300
Supersedes and closes #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 fix(security): close SSRF bypass via IPv6, CGNAT, and IPv4-mapped ranges in config set #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

  • 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

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • 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 (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

AI Disclosure

  • AI-assisted — tool: Claude Code

Signed-off-by: Tinson Lai tinsonl@nvidia.com

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.

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 #2300 to exist cannot recur.

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 #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.

Fixes #2300
Supersedes and closes #2305

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 9ee40176-ff86-4685-9c9c-beeb09e653f7

📥 Commits

Reviewing files that changed from the base of the PR and between 39e49ba and 18f9682.

📒 Files selected for processing (4)
  • nemoclaw/src/blueprint/private-networks.test.ts
  • nemoclaw/src/blueprint/private-networks.ts
  • src/lib/private-networks.ts
  • test/ssrf-parity.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/ssrf-parity.test.ts
  • nemoclaw/src/blueprint/private-networks.test.ts

📝 Walkthrough

Walkthrough

Adds a canonical SSRF-deny YAML, runtime loaders in blueprint and CLI/library, strict schema validation, memoized node:net BlockList and normalized reserved-name list, new exported accessors (getNetworkEntries/getPrivateNetworks/isPrivateIp/isPrivateHostname/resetCache), and extensive tests and parity checks. (28 words)

Changes

Cohort / File(s) Summary
Blueprint YAML
nemoclaw-blueprint/private-networks.yaml
Adds canonical SSRF-deny document enumerating ipv4/ipv6 CIDRs and names with required non-empty purpose fields and consumer guidance.
Nemoclaw blueprint module & tests
nemoclaw/src/blueprint/private-networks.ts, nemoclaw/src/blueprint/private-networks.test.ts, nemoclaw/src/blueprint/ssrf.ts, nemoclaw/src/blueprint/ssrf.test.ts
New blueprint loader resolving YAML (env/dev-guess/cwd), strict runtime schema validation, builds/memoizes a node:net BlockList and normalized name list; exports getters, resetCache, isPrivateIp, isPrivateHostname; replaces prior in-file CIDR logic and expands tests to cover translation/reserved ranges and hostname matching rules.
CLI / Library implementation & integration
src/lib/private-networks.ts, src/lib/sandbox-config.ts
Adds CLI-side implementation mirroring blueprint behavior (validation, memoized BlockList, normalized names, exported API). Integrates isPrivateHostname into sandbox-config URL validation and tightens trimming/case handling to avoid scheme/whitespace bypasses.
Parity and coverage tests
test/ssrf-parity.test.ts
Adds parity guard that loads built CLI and Nemoclaw helpers, asserts schema and fingerprint parity, enforces non-empty purpose, checks duplicate-free normalized names, and runs extensive per-CIDR boundary and hostname behavioral vectors across implementations.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant SSRF_Module as "ssrf.ts"
  participant PrivateNetworks as "private-networks(.ts)"
  participant FileSystem as "private-networks.yaml"
  participant BlockList as "node:net BlockList"

  Client->>SSRF_Module: validate endpoint / hostname
  SSRF_Module->>PrivateNetworks: isPrivateHostname(hostname)
  PrivateNetworks->>FileSystem: resolve & read YAML (env/dev/cwd) [cached]
  FileSystem-->>PrivateNetworks: YAML document
  PrivateNetworks->>PrivateNetworks: validate schema & build memoized entries
  PrivateNetworks->>BlockList: construct/add CIDRs
  BlockList-->>PrivateNetworks: membership checks
  PrivateNetworks-->>SSRF_Module: boolean (private or not)
  SSRF_Module-->>Client: validation result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble YAML lines at dawn,
I hop through CIDRs, neat and drawn,
Brackets brushed, names trimmed so bright,
I keep the tunnels closed at night,
Tests hum — the meadow’s safe and warm.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: establishing a shared private-network block list between CLI and plugin using a canonical YAML data file.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/2300-ssrf-blocklist

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/sandbox-config.ts (1)

324-336: ⚠️ Potential issue | 🟠 Major

Run validateUrlValue() for every string, not only lower-case http(s) prefixes.

The guard on Lines 325-327 lets valid URLs like HTTP://127.0.0.1, Https://localhost, or whitespace-padded inputs skip SSRF validation entirely. validateUrlValue() already no-ops for non-URLs, so this prefix check only creates a bypass.

Suggested fix
-  if (
-    typeof parsedValue === "string" &&
-    (parsedValue.startsWith("http://") || parsedValue.startsWith("https://"))
-  ) {
+  if (typeof parsedValue === "string") {
     try {
       validateUrlValue(parsedValue);
     } catch (err: unknown) {
       const message = err instanceof Error ? err.message : String(err);
       console.error(`  URL validation failed: ${message}`);
🤖 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 324 - 336, The current guard only
runs validateUrlValue when parsedValue starts with lower-case "http://" or
"https://", letting mixed-case or whitespace-padded URLs bypass SSRF checks;
change the logic so that for any typeof parsedValue === "string" you call
validateUrlValue (after trimming the string) inside the existing try/catch.
Specifically, replace the startsWith checks with a simple typeof parsedValue ===
"string" condition, call validateUrlValue(parsedValue.trim()), and keep the
existing error handling that logs the message and exits; reference parsedValue
and validateUrlValue to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nemoclaw/src/blueprint/private-networks.ts`:
- Around line 49-56: resolveBlueprintPath() currently returns the devGuess
directory if it exists even when the required private-networks.yaml isn't
present, causing load() to fail; update resolveBlueprintPath to only return
devGuess when both the directory exists and the file join(devGuess,
"private-networks.yaml") exists (use existsSync), otherwise fall back to the
current default ("."); refer to the resolveBlueprintPath function in
private-networks.ts and the subsequent load() call so the check ensures the YAML
is present before returning the dev checkout path.

In `@test/ssrf-parity.test.ts`:
- Around line 36-38: The parity guard currently omits validating
names[*].purpose: update the NetworkHelper API returned by getNetworkEntries()
to include the names array (e.g., add a names: NetworkEntryName[] or similar
shape) and then extend the assertion loop that iterates ipv4/ipv6 to also
iterate each entry in names and assert that name.purpose is present and
non-empty; update any type references and the test assertions that use
NetworkHelper/getNetworkEntries() and isPrivateHostname to ensure
names[*].purpose is checked (mirror the ipv4/ipv6 validation logic).

---

Outside diff comments:
In `@src/lib/sandbox-config.ts`:
- Around line 324-336: The current guard only runs validateUrlValue when
parsedValue starts with lower-case "http://" or "https://", letting mixed-case
or whitespace-padded URLs bypass SSRF checks; change the logic so that for any
typeof parsedValue === "string" you call validateUrlValue (after trimming the
string) inside the existing try/catch. Specifically, replace the startsWith
checks with a simple typeof parsedValue === "string" condition, call
validateUrlValue(parsedValue.trim()), and keep the existing error handling that
logs the message and exits; reference parsedValue and validateUrlValue to locate
the code to change.
🪄 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: f152327f-d496-4851-a373-fd651a6f27fb

📥 Commits

Reviewing files that changed from the base of the PR and between d9aced4 and 53595a6.

📒 Files selected for processing (8)
  • nemoclaw-blueprint/private-networks.yaml
  • nemoclaw/src/blueprint/private-networks.test.ts
  • nemoclaw/src/blueprint/private-networks.ts
  • nemoclaw/src/blueprint/ssrf.test.ts
  • nemoclaw/src/blueprint/ssrf.ts
  • src/lib/private-networks.ts
  • src/lib/sandbox-config.ts
  • test/ssrf-parity.test.ts

Comment thread nemoclaw/src/blueprint/private-networks.ts
Comment thread test/ssrf-parity.test.ts
* private-networks.ts resolveBlueprintPath: check for the
  private-networks.yaml file specifically instead of just the
  nemoclaw-blueprint directory. A stale checkout with the directory
  but no yaml now falls through to cwd instead of failing load().
* ssrf-parity.test.ts: extend the non-empty-purpose loop and the
  duplicate-entry check to cover the names array, so a blank
  names[*].purpose or a duplicate name can no longer slip through.
* sandbox-config.ts validateUrlValue guard: drop the lowercase
  startsWith("http://"|"https://") pre-filter, which let mixed-case
  schemes (HTTP://127.0.0.1) and whitespace-padded URLs bypass
  validation. Trim the string and hand every string to
  validateUrlValue, which already no-ops for non-URL input.

Plus a matching dev-guess-fallback test in the plugin's
private-networks.test.ts.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/ssrf-parity.test.ts`:
- Around line 83-90: The duplicate-check currently builds the name keys from
doc.names using e.name.toLowerCase() but doesn't remove a trailing dot, so names
like "localhost" and "localhost." slip past; update the key creation for names
in the keys array to normalize exactly like runtime by lowercasing and stripping
a single trailing dot (e.g., transform e.name via lowercasing then remove a
trailing '.' before building the `name:${...}` entry), leaving the IPv4/IPv6 key
logic unchanged.
🪄 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: 5fd826a5-fa2e-4110-94d9-f52a6c724704

📥 Commits

Reviewing files that changed from the base of the PR and between 53595a6 and cb1ed33.

📒 Files selected for processing (4)
  • nemoclaw/src/blueprint/private-networks.test.ts
  • nemoclaw/src/blueprint/private-networks.ts
  • src/lib/sandbox-config.ts
  • test/ssrf-parity.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • nemoclaw/src/blueprint/private-networks.test.ts
  • src/lib/sandbox-config.ts
  • nemoclaw/src/blueprint/private-networks.ts

Comment thread test/ssrf-parity.test.ts
Match the runtime name-normalisation in isPrivateHostname, so the
duplicate-entry guard catches a name that appears with and without
the trailing FQDN dot (e.g. localhost vs localhost.).

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
test/ssrf-parity.test.ts (2)

46-47: Add clearer failure diagnostics for missing build artifacts.
Direct require() from dist paths fails with a generic module error; wrapping loads with a small helper gives contributors an immediate build hint and reduces triage time.

Suggested change
 const require = createRequire(import.meta.url);
@@
-const cliHelper = require("../dist/lib/private-networks") as NetworkHelper;
-const pluginHelper = require("../nemoclaw/dist/blueprint/private-networks.js") as NetworkHelper;
+function loadHelper(path: string, hint: string): NetworkHelper {
+  try {
+    return require(path) as NetworkHelper;
+  } catch {
+    throw new Error(`Failed to load '${path}'. ${hint}`);
+  }
+}
+
+const cliHelper = loadHelper("../dist/lib/private-networks", "Run `npm run build:cli` first.");
+const pluginHelper = loadHelper(
+  "../nemoclaw/dist/blueprint/private-networks.js",
+  "Run `npm run build` in `nemoclaw/` first.",
+);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ssrf-parity.test.ts` around lines 46 - 47, The test currently does
direct requires for dist artifacts (cliHelper and pluginHelper) which surface
generic module-not-found errors; create and use a small loader helper (e.g.,
loadDist or requireWithBuildHint) that attempts require(path) and on failure
throws or logs a clear message telling the contributor to run the build (include
the original error message for diagnostics), then replace the direct requires of
"../dist/lib/private-networks" and
"../nemoclaw/dist/blueprint/private-networks.js" with calls to that helper so
failures show the actionable build hint and original error details.

105-274: Consider deriving CIDR boundary vectors from getNetworkEntries() to prevent stale coverage.
The current matrix is strong, but manual vectors can drift when the YAML changes. Generating vectors from parsed CIDRs keeps boundary coverage automatically aligned with the canonical block list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/ssrf-parity.test.ts` around lines 105 - 274, The hardcoded vectors array
in test/ssrf-parity.test.ts can become stale; instead generate boundary test
vectors from the canonical CIDRs returned by getNetworkEntries(): iterate
getNetworkEntries() (the same function that parses the YAML), and for each
NetworkEntry produce the start, end, quarter, three-quarter and optional
before/after addresses (handling IPv4 vs IPv6 appropriately) and replace the
static vectors variable with this generated list so tests stay in sync with the
source CIDR list; update any test consumer of vectors to accept the generated
format and ensure ordering/labels remain stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/ssrf-parity.test.ts`:
- Around line 46-47: The test currently does direct requires for dist artifacts
(cliHelper and pluginHelper) which surface generic module-not-found errors;
create and use a small loader helper (e.g., loadDist or requireWithBuildHint)
that attempts require(path) and on failure throws or logs a clear message
telling the contributor to run the build (include the original error message for
diagnostics), then replace the direct requires of "../dist/lib/private-networks"
and "../nemoclaw/dist/blueprint/private-networks.js" with calls to that helper
so failures show the actionable build hint and original error details.
- Around line 105-274: The hardcoded vectors array in test/ssrf-parity.test.ts
can become stale; instead generate boundary test vectors from the canonical
CIDRs returned by getNetworkEntries(): iterate getNetworkEntries() (the same
function that parses the YAML), and for each NetworkEntry produce the start,
end, quarter, three-quarter and optional before/after addresses (handling IPv4
vs IPv6 appropriately) and replace the static vectors variable with this
generated list so tests stay in sync with the source CIDR list; update any test
consumer of vectors to accept the generated format and ensure ordering/labels
remain stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c3e45b52-2da3-4534-bfa6-d86fb4b856ad

📥 Commits

Reviewing files that changed from the base of the PR and between cb1ed33 and 703ba1f.

📒 Files selected for processing (1)
  • test/ssrf-parity.test.ts

@wscurran wscurran added bug Something isn't working security Something isn't secure priority: high Important issue that should be resolved in the next release labels Apr 23, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this issue that identifies a bug with the private network block lists and proposes a fix.


Related open PRs:


Related open issues:

Copy link
Copy Markdown
Contributor

@ericksoa ericksoa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this one, Tinson. The architecture is sound — single YAML source of truth with a parity test is exactly the right approach, and the coverage expansion (CGNAT, translation prefixes, reserved TLDs) is substantial.

One small change request, then a few items for follow-up.

Requested change

Add prefix-length upper-bound validation. Both loaders validate prefix >= 0 but never check prefix <= 32 (IPv4) or prefix <= 128 (IPv6). A typo like prefix: 33 on an IPv4 entry would be silently passed to BlockList.addSubnet with undefined behavior.

In both src/lib/private-networks.ts and nemoclaw/src/blueprint/private-networks.ts, the validateNetworkEntry function should include:

const maxPrefix = family === "ipv4" ? 32 : 128;
if (typeof prefix !== "number" || !Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) {

Follow-up items (non-blocking)

  1. Code duplication between CLI and plugin loaders — the two private-networks.ts files share ~90% identical code. Any future logic change (like this prefix bound) must be applied in both. Worth considering whether a shared internal package or build-time copy could reduce the maintenance surface.

  2. Parity test depends on pre-built artifactstest/ssrf-parity.test.ts imports from dist/. A developer who forgets to build first gets a confusing MODULE_NOT_FOUND. A guard with a descriptive error message would improve DX.

  3. readFileSync error on missing YAML — a missing file produces a raw ENOENT. A one-line existence check before the read with a hint about NEMOCLAW_BLUEPRINT_PATH would help.

  4. 192.0.1.0/24 gap192.0.0.0/24 and 192.0.2.0/24 are both blocked but the /24 in between is not. If intentional, a comment in the YAML noting the gap would be helpful.

@laitingsheng
Copy link
Copy Markdown
Contributor Author

4. 192.0.1.0/24 gap192.0.0.0/24 and 192.0.2.0/24 are both blocked but the /24 in between is not. If intentional, a comment in the YAML noting the gap would be helpful.

192.0.1.0/24 is actually a public CIDR range, which looks pretty awkward.

I will try to address the rest.

@laitingsheng
Copy link
Copy Markdown
Contributor Author

  • Code duplication between CLI and plugin loaders — the two private-networks.ts files share ~90% identical code. Any future logic change (like this prefix bound) must be applied in both. Worth considering whether a shared internal package or build-time copy could reduce the maintenance surface.
  • Parity test depends on pre-built artifactstest/ssrf-parity.test.ts imports from dist/. A developer who forgets to build first gets a confusing MODULE_NOT_FOUND. A guard with a descriptive error message would improve DX.

Regarding 1, since there is a boundary for plugin and CLI, I will hold the changes since it needs more complex refactorings, and I think it is already out of the scope of this PR. But I totally agree that we need to deduplicate the code to avoid any possibile mistakes even though we have parity tests.

This could be beneficial for 2, but I would just improve it a bit according to your suggestions for this PR.

- validateNetworkEntry now enforces prefix ∈ [0, 32] for IPv4 and
  [0, 128] for IPv6. Previously an out-of-range value like prefix: 33
  was passed through to BlockList.addSubnet with undefined behaviour.
  Applied symmetrically to the CLI loader (src/lib/private-networks.ts)
  and the plugin loader (nemoclaw/src/blueprint/private-networks.ts).
- Both loaders now existsSync-precheck the resolved YAML path and
  throw a message naming the path and NEMOCLAW_BLUEPRINT_PATH, instead
  of a raw ENOENT from readFileSync.
- test/ssrf-parity.test.ts wraps its two dist/ require() calls in a
  helper that catches MODULE_NOT_FOUND and points at the relevant
  build command, replacing the confusing generic module error when a
  developer forgets to build first.
- nemoclaw-blueprint/private-networks.yaml gains a comment explaining
  that the gap at 192.0.1.0/24 is intentional — that /24 is globally
  routable APNIC space, not an IANA special-purpose assignment like
  the /24 blocks either side of it.

Tests extended with coverage for negative, fractional, too-big IPv4
(>32), and too-big IPv6 (>128) prefixes, plus a missing-YAML error
message assertion. The CLI-plugin loader duplication item from the
review is tracked as a follow-up — the right shape is either a pure-
functions core with two thin wrappers or a full collapse, and the
decision belongs in its own PR.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
…x/2300-ssrf-blocklist

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/private-networks.ts`:
- Around line 103-111: The error thrown in load() referencing
NEMOCLAW_BLUEPRINT_PATH is misleading because this module uses ROOT (from
./runner) and resolves the blueprint at build/compile time; update the message
thrown when NETWORKS_FILE is missing to explain that the file is sought relative
to the compiled project root (ROOT) and suggest ensuring the blueprint is
present in the repository/checkout or adjusting the build/checkout location, not
setting an environment variable; locate the load() function and replace the
incorrect env-var guidance with wording that mentions ROOT or the compiled
project root and how to place the nemoclaw-blueprint/ directory so NETWORKS_FILE
can be found.

In `@test/ssrf-parity.test.ts`:
- Around line 46-59: The catch block in loadHelper uses the undefined NodeJS
type and discards the original error; change the error typing to unknown (e.g.,
const err = error as unknown as { code?: string } or narrow from unknown) and
check err.code for "MODULE_NOT_FOUND", and when throwing the new Error include
the original error as the cause (using new Error(message, { cause: error })) so
the stack context is preserved; update references in loadHelper, modulePath,
buildHint, and err accordingly.
🪄 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: Enterprise

Run ID: 159b5b7d-176b-4365-8e1f-67ac5d300c7e

📥 Commits

Reviewing files that changed from the base of the PR and between 703ba1f and 39e49ba.

📒 Files selected for processing (5)
  • nemoclaw-blueprint/private-networks.yaml
  • nemoclaw/src/blueprint/private-networks.test.ts
  • nemoclaw/src/blueprint/private-networks.ts
  • src/lib/private-networks.ts
  • test/ssrf-parity.test.ts
✅ Files skipped from review due to trivial changes (1)
  • nemoclaw-blueprint/private-networks.yaml

Comment thread src/lib/private-networks.ts
Comment thread test/ssrf-parity.test.ts
- src/lib/private-networks.ts: The ENOENT error added in 4efbc76
  suggested setting NEMOCLAW_BLUEPRINT_PATH as a remedy, but the CLI
  loader resolves its path via ROOT (path.resolve(__dirname, "..", ".."))
  and does not consult that env var — only the plugin loader does.
  Replace the suggestion with guidance that matches how the CLI
  actually resolves the file.
- test/ssrf-parity.test.ts: The loadHelper introduced in 4efbc76
  used `NodeJS.ErrnoException` (flagged by eslint no-undef in ESM
  context) and discarded the original error (flagged by
  preserve-caught-error). Narrow the type to `{ code?: unknown }` and
  preserve the underlying cause via `{ cause: error }`.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/lib/private-networks.ts (1)

45-67: Validate the IP family during the schema pass.

address is only checked for non-empty string here, so a malformed literal or an IPv6 entry under ipv4 would fail later in load() and lose the ...ipv4[i] / ...ipv6[i] context you already built. Validating isIP(address) against family here keeps YAML errors precise.

Suggested change
 function validateNetworkEntry(entry: unknown, family: "ipv4" | "ipv6", index: number): NetworkEntry {
   const where = `${NETWORKS_FILE}: ${family}[${String(index)}]`;
   if (typeof entry !== "object" || entry === null) {
     throw new Error(`${where}: expected an object`);
   }
   const record = entry as Record<string, unknown>;
   const address = record.address;
   const prefix = record.prefix;
   const purpose = record.purpose;
   if (typeof address !== "string" || address.length === 0) {
     throw new Error(`${where}: missing or empty 'address'`);
   }
+  const expectedFamily = family === "ipv4" ? 4 : 6;
+  if (isIP(address) !== expectedFamily) {
+    throw new Error(`${where}: 'address' must be a valid ${family} literal`);
+  }
   const maxPrefix = family === "ipv4" ? 32 : 128;
   if (typeof prefix !== "number" || !Number.isInteger(prefix) || prefix < 0 || prefix > maxPrefix) {
     throw new Error(
       `${where}: 'prefix' must be an integer in [0, ${String(maxPrefix)}], got ${JSON.stringify(prefix)}`,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/private-networks.ts` around lines 45 - 67, validateNetworkEntry
currently only checks that address is a non-empty string but not that it matches
the declared family; update validateNetworkEntry to call the IP validation
helper (e.g., isIP or equivalent) to assert that address is a valid IPv4 when
family === "ipv4" and a valid IPv6 when family === "ipv6", and if the check
fails throw a descriptive Error including the where variable and the family (so
messages like `${where}: address '${address}' is not a valid ${family}
address`); keep existing checks for prefix and purpose and return the same
NetworkEntry shape so later load() no longer has to re-associate family-specific
context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/lib/private-networks.ts`:
- Around line 45-67: validateNetworkEntry currently only checks that address is
a non-empty string but not that it matches the declared family; update
validateNetworkEntry to call the IP validation helper (e.g., isIP or equivalent)
to assert that address is a valid IPv4 when family === "ipv4" and a valid IPv6
when family === "ipv6", and if the check fails throw a descriptive Error
including the where variable and the family (so messages like `${where}: address
'${address}' is not a valid ${family} address`); keep existing checks for prefix
and purpose and return the same NetworkEntry shape so later load() no longer has
to re-associate family-specific context.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0e85c81e-85e8-494f-b9c4-4fd90e5050fd

📥 Commits

Reviewing files that changed from the base of the PR and between 39e49ba and 6b02ddd.

📒 Files selected for processing (2)
  • src/lib/private-networks.ts
  • test/ssrf-parity.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/ssrf-parity.test.ts

Both loaders now reject an entry whose address doesn't parse as the
declared family — an IPv6 literal under ipv4 (or vice versa), or a
malformed string. Previously typeof/length was the only guard, so a
typo like placing "::1" under ipv4 silently passed validation and
later surfaced as a confusing runtime error from BlockList.addSubnet
with no ipv4[i] / ipv6[i] location context.

Applied symmetrically to src/lib/private-networks.ts and
nemoclaw/src/blueprint/private-networks.ts so the parity test
continues to hold. Tests added for all three rejection paths:
IPv6-under-ipv4, IPv4-under-ipv6, and a non-IP-literal string.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@laitingsheng laitingsheng requested a review from ericksoa April 24, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working priority: high Important issue that should be resolved in the next release security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incomplete SSRF validation in config set — missing CGNAT, IPv6, and IPv4-mapped ranges

3 participants