Skip to content

fix(security): prevent prototype pollution via unsanitized config path [MEDIUM]#1558

Merged
ericksoa merged 6 commits intoNVIDIA:mainfrom
Joshua-Medvinsky:fix/prototype-pollution-config-path
Apr 16, 2026
Merged

fix(security): prevent prototype pollution via unsanitized config path [MEDIUM]#1558
ericksoa merged 6 commits intoNVIDIA:mainfrom
Joshua-Medvinsky:fix/prototype-pollution-config-path

Conversation

@Joshua-Medvinsky
Copy link
Copy Markdown
Contributor

@Joshua-Medvinsky Joshua-Medvinsky commented Apr 7, 2026

Security Finding: Prototype Pollution via Unsanitized Config Path

Severity: MEDIUM
Reported by: FailSafe Security Researcher
Component: nemoclaw/src/commands/migration-state.tssetConfigValue()

Description

The setConfigValue() function in the snapshot migration module tokenizes a dot-separated configuration path and iteratively traverses or builds an object structure to set a value. The function does not sanitize path tokens against dangerous JavaScript property names such as __proto__, constructor, or prototype, allowing an attacker to pollute Object.prototype through a crafted snapshot manifest.

The configPath value originates from manifest.externalRoots[].bindings[].configPath inside the snapshot's snapshot.json file. When an operator restores a snapshot from an untrusted or compromised source, the manifest-controlled configPath is passed directly to setConfigValue() without filtering.

Exploitation path: A crafted snapshot.json with a binding containing configPath: "__proto__.isAdmin" would cause setConfigValue() to traverse into Object.prototype and set isAdmin on every object in the Node.js process.

Fix

Add a denylist check (__proto__, constructor, prototype) before using each token as a property key. Throws an error if any token matches a dangerous key.

Test plan

  • Verify snapshot restore with legitimate configPath values still works
  • Verify configPath: "__proto__.isAdmin" throws Unsafe config path segment
  • Verify configPath: "constructor.polluted" is rejected
  • Verify configPath: "foo.prototype.bar" is rejected

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced configuration validation to reject unsafe path segments (e.g., proto, constructor, prototype), preventing prototype-pollution and unsafe mutations. Validation now runs before applying changes for safer, more reliable config updates.
  • Tests
    • Added tests that confirm unsafe paths are rejected and that safe top-level, nested, and indexed/dotted configuration paths are written successfully.

Signed-off-by: Joshua Medvinsky joshuam@ethlas.com

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a module-level UNSAFE_PROPERTY_NAMES set and exports setConfigValue; setConfigValue now pre-validates all parsed config-path tokens and throws Error("Unsafe config path segment ...") for __proto__, constructor, or prototype before mutating the document.

Changes

Cohort / File(s) Summary
Migration state implementation
nemoclaw/src/commands/migration-state.ts
Introduce UNSAFE_PROPERTY_NAMES (__proto__, constructor, prototype); make setConfigValue exported and add pre-traversal validation that throws on unsafe path segments. Existing traversal/creation and assignment logic preserved.
Tests
nemoclaw/src/commands/migration-state.test.ts
Add describe("setConfigValue") tests: assert errors for unsafe segments in top-level, nested, and dotted/indexed paths; assert successful writes for allowed dotted/index-like and top-level paths; verify Object.prototype remains unpolluted.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hop through keys with cautious paws,
Sniff out __proto__, constructor, prototype flaws,
A twitch, a guard — I stop the spread,
I plant safe names and tuck threads to bed,
🥕 Happy configs, no surprises in the laws.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main security fix: preventing prototype pollution via unsanitized config paths in the setConfigValue function.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

wscurran commented Apr 8, 2026

✨ Thanks for submitting this fix, which proposes a way to prevent prototype pollution by validating config path tokens during snapshot restoration.

cv
cv previously requested changes Apr 8, 2026
Copy link
Copy Markdown
Contributor

@cv cv left a comment

Choose a reason for hiding this comment

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

Security Review — WARNING

The code change is sound — the denylist approach is correct, validation runs upfront before any traversal (no partial mutation), and false positive risk is negligible.

Required change

Add regression tests. A security fix without tests can silently regress. At minimum:

  1. A configPath containing __proto__ throws "Unsafe config path segment"
  2. A configPath containing constructor throws
  3. Legitimate paths like agents.list[0].workspace still work

setConfigValue is private, so test via prepareSandboxState or export it for direct testing.

What's good

  • Threat is real — crafted snapshot manifest can pollute Object.prototype via bracket-access traversal
  • Fix is at the right layer (input validation in setConfigValue before traversal)
  • All tokens validated upfront in a separate loop — no partial mutation on error
  • Error message includes token + path for debugging without leaking sensitive info
  • DANGEROUS_KEYS as a frozen Set at module scope is clean

@Joshua-Medvinsky Joshua-Medvinsky force-pushed the fix/prototype-pollution-config-path branch from 5ef98d7 to deb4a1d Compare April 8, 2026 23:29
@Joshua-Medvinsky
Copy link
Copy Markdown
Contributor Author

Updated per review feedback:

  • Added regression tests: __proto__, constructor, and prototype all throw Unsafe config path segment
  • Added test for nested position (agents.__proto__.isAdmin)
  • Added tests confirming legitimate paths (agents.list[0].workspace, simple keys) still work
  • Exported setConfigValue with @visibleForTesting for direct test coverage

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 `@nemoclaw/src/commands/migration-state.test.ts`:
- Around line 1425-1440: The tests currently check nested "__proto__" but miss
nested "constructor" and "prototype" cases; update the migration-state tests
that call setConfigValue so they also assert nested unsafe segments are rejected
(e.g., add expectations like expect(() => setConfigValue(doc,
`agents.constructor.isAdmin`, "true")).toThrow(/Unsafe config path segment/) and
expect(() => setConfigValue(doc, `agents.prototype.isAdmin`,
"true")).toThrow(/Unsafe config path segment/)); you can either add two new
it(...) blocks mirroring the "__proto__" nested case or extend the existing
it.each/array of segments used with setConfigValue to include "constructor" and
"prototype" so setConfigValue is validated for all three unsafe segments.
🪄 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

Run ID: c843840d-e9a3-4ed4-8b51-0da79dab639d

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef98d7 and deb4a1d.

📒 Files selected for processing (2)
  • nemoclaw/src/commands/migration-state.test.ts
  • nemoclaw/src/commands/migration-state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • nemoclaw/src/commands/migration-state.ts

Comment thread nemoclaw/src/commands/migration-state.test.ts
@Joshua-Medvinsky
Copy link
Copy Markdown
Contributor Author

@cv Following up on the fix above, added regression tests per ask

@wscurran wscurran added the status: rebase PR needs to be rebased against main before review can continue label Apr 14, 2026
…h [MEDIUM]

setConfigValue() tokenized dot-separated config paths from snapshot
manifests and traversed into objects without checking for dangerous
property names. A crafted snapshot with configPath "__proto__.isAdmin"
could pollute Object.prototype, corrupting process-wide state.

Add UNSAFE_PROPERTY_NAMES denylist check that rejects __proto__,
constructor, and prototype before any traversal occurs. Add regression
tests covering all three dangerous keys in both top-level and nested
positions, plus legitimate path formats.

Reported-by: FailSafe Security Researcher
Co-Authored-By: Joshua Medvinsky <joshua-medvinsky@users.noreply.github.com>
@Joshua-Medvinsky Joshua-Medvinsky force-pushed the fix/prototype-pollution-config-path branch from deb4a1d to 7e1ee6f Compare April 15, 2026 05:44
@Joshua-Medvinsky
Copy link
Copy Markdown
Contributor Author

Rebased onto current main and addressed all feedback:

  • Rebased cleanly onto main (post TypeScript migration refactor(ts-migration): move onboard and cli to typescript #1673)
  • Added nested constructor/prototype test cases per coderabbit feedback (foo.prototype.bar, foo.constructor.bar)
  • All three dangerous keys tested in both top-level and nested positions
  • Legitimate paths confirmed working

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)
nemoclaw/src/commands/migration-state.test.ts (1)

1425-1451: Assert “no prototype mutation” in reject-path tests.

These tests currently verify throws, but not that global prototype state remained unchanged. Add a post-assertion probe so mutate-then-throw regressions are caught.

✅ Suggested hardening for the regression tests
   describe("setConfigValue", () => {
+    const expectPrototypeClean = (): void => {
+      const probe: Record<string, unknown> = {};
+      expect(probe["polluted"]).toBeUndefined();
+      expect(probe["isAdmin"]).toBeUndefined();
+      expect(probe["bar"]).toBeUndefined();
+    };
+
     it.each(["__proto__", "constructor", "prototype"])(
       "rejects unsafe path segment: %s",
       (segment) => {
         const doc: Record<string, unknown> = {};
         expect(() => setConfigValue(doc, `${segment}.polluted`, "true")).toThrow(
           /Unsafe config path segment/,
         );
+        expectPrototypeClean();
       },
     );

     it("rejects __proto__ in nested position", () => {
       const doc: Record<string, unknown> = {};
       expect(() =>
         setConfigValue(doc, "agents.__proto__.isAdmin", "true"),
       ).toThrow(/Unsafe config path segment/);
+      expectPrototypeClean();
     });

     it.each(["foo.prototype.bar", "foo.constructor.bar"])(
       "rejects unsafe segment in nested path: %s",
       (configPath) => {
         const doc: Record<string, unknown> = {};
         expect(() => setConfigValue(doc, configPath, "true")).toThrow(
           /Unsafe config path segment/,
         );
+        expectPrototypeClean();
       },
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/commands/migration-state.test.ts` around lines 1425 - 1451, The
tests for setConfigValue must also assert that global prototypes were not
mutated after the call: after each expect(...).toThrow in the unsafe-path tests
(including the each cases and the nested cases), add a post-assertion that
checks Object.prototype and a new plain object do not have the injected
properties (e.g., ensure Object.prototype.polluted/isAdmin/other test keys are
undefined and that ({ } as any).polluted is undefined), so a mutate-then-throw
regression will fail; reference the existing test block and the setConfigValue
calls to place these probes immediately after each thrown-expect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nemoclaw/src/commands/migration-state.test.ts`:
- Around line 1425-1451: The tests for setConfigValue must also assert that
global prototypes were not mutated after the call: after each
expect(...).toThrow in the unsafe-path tests (including the each cases and the
nested cases), add a post-assertion that checks Object.prototype and a new plain
object do not have the injected properties (e.g., ensure
Object.prototype.polluted/isAdmin/other test keys are undefined and that ({ } as
any).polluted is undefined), so a mutate-then-throw regression will fail;
reference the existing test block and the setConfigValue calls to place these
probes immediately after each thrown-expect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b75b7a0-98d5-4570-af4d-ed73e385c5b1

📥 Commits

Reviewing files that changed from the base of the PR and between deb4a1d and 7e1ee6f.

📒 Files selected for processing (2)
  • nemoclaw/src/commands/migration-state.test.ts
  • nemoclaw/src/commands/migration-state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • nemoclaw/src/commands/migration-state.ts

@cv cv added the v0.0.17 label Apr 15, 2026
@wscurran wscurran removed the status: rebase PR needs to be rebased against main before review can continue label Apr 15, 2026
Replace `as any` cast with typed narrowing and add braces to void
arrow returns to satisfy no-explicit-any and no-confusing-void-expression.

Signed-off-by: Test User <test@example.com>
@ericksoa
Copy link
Copy Markdown
Contributor

Thanks for the fix! I pushed a small commit (819b098a) to clean up the ESLint errors so CI can get past the checks gate.

One last thing — do you see the dco-check failure? The PR description needs a Signed-off-by line for DCO compliance. Could you add one to the body? Format is:

Signed-off-by: Your Name <your-email@example.com>

Once that's green we're good to approve and merge. Thanks for the solid root-cause analysis on this one!

@Joshua-Medvinsky
Copy link
Copy Markdown
Contributor Author

Thanks for picking up the ESLint cleanup — appreciate it.

Added the DCO sign-off to the PR description, should be green now.

One question — now that all three security fixes are merged (#1557, #1559) or about to merge (#1558), is there a process on your side for issuing CVEs? We've already reached out to PSIRT@nvidia.com but wanted to check if there's a preferred route through the maintainer team. Happy to provide CVSS breakdowns or any additional detail needed for the advisories.

@ericksoa ericksoa added v0.0.18 Release target and removed v0.0.17 labels Apr 16, 2026
@ericksoa ericksoa reopened this Apr 16, 2026
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@ericksoa
Copy link
Copy Markdown
Contributor

Thanks for picking up the ESLint cleanup — appreciate it.

Added the DCO sign-off to the PR description, should be green now.

One question — now that all three security fixes are merged (#1557, #1559) or about to merge (#1558), is there a process on your side for issuing CVEs? We've already reached out to PSIRT@nvidia.com but wanted to check if there's a preferred route through the maintainer team. Happy to provide CVSS breakdowns or any additional detail needed for the advisories.

PSIRT process is ideal, than you for doing that - that team will take things from there.

@ericksoa ericksoa dismissed cv’s stale review April 16, 2026 21:45

Tests were added in subsequent commits (including by cv himself in 07bc4a4). Dismissing stale changes-requested review.

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.

Security fix is sound — denylist validates upfront before any traversal, tests cover all three dangerous keys in top-level and nested positions, and prototype-clean assertions confirm no partial mutation. Prettier formatting fixed. LGTM.

@ericksoa ericksoa merged commit 6c57818 into NVIDIA:main Apr 16, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix priority: high Important issue that should be resolved in the next release security Something isn't secure v0.0.18 Release target

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants