Skip to content

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

Open
Joshua-Medvinsky wants to merge 1 commit intoNVIDIA:mainfrom
Joshua-Medvinsky:fix/prototype-pollution-config-path
Open

fix(security): prevent prototype pollution via unsanitized config path [MEDIUM]#1558
Joshua-Medvinsky wants to merge 1 commit 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 block unsafe path segments, preventing prototype-pollution and other unsafe mutations for safer configuration handling.
  • Tests
    • Added tests covering rejected unsafe path segments and successful writes for safe top-level, nested, and indexed/dotted configuration paths.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Walkthrough

Introduces a module-level UNSAFE_PROPERTY_NAMES set and makes setConfigValue exported; setConfigValue now validates all config-path tokens and throws Error("Unsafe config path segment ...") if any token is __proto__, constructor, or prototype before mutating the document.

Changes

Cohort / File(s) Summary
Migration state implementation
nemoclaw/src/commands/migration-state.ts
Added UNSAFE_PROPERTY_NAMES (__proto__, constructor, prototype); changed setConfigValue to export and added pre-traversal validation that throws on unsafe path segments. Core traversal/assignment logic unchanged.
Tests
nemoclaw/src/commands/migration-state.test.ts
Added tests for setConfigValue covering rejection of unsafe segments (including nested/dotted paths) and successful writes for safe dotted/index-like and top-level paths.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through tokens, nose held high,
Sniffing __proto__ that try to pry,
I thump my foot and sound the bell,
"No prototype tricks — not here, not well!" 🥕

🚥 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 in config path handling.

✏️ 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.

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

…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 and 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 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 on lines +1425 to +1440
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/,
);
},
);

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add nested constructor/prototype cases to match the security test plan.

Current coverage checks nested __proto__, but not nested constructor/prototype paths (for example foo.prototype.bar), which were called out in the PR objective.

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

+  it.each(["foo.prototype.bar", "foo.constructor.bar"])(
+    "rejects unsafe segment in nested path: %s",
+    (path) => {
+      const doc: Record<string, unknown> = {};
+      expect(() => setConfigValue(doc, path, "true")).toThrow(
+        /Unsafe config path segment/,
+      );
+    },
+  );
🤖 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 - 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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants