fix(security): prevent prototype pollution via unsanitized config path [MEDIUM]#1558
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a module-level Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
✨ Thanks for submitting this fix, which proposes a way to prevent prototype pollution by validating config path tokens during snapshot restoration. |
cv
left a comment
There was a problem hiding this comment.
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:
- A
configPathcontaining__proto__throws"Unsafe config path segment" - A
configPathcontainingconstructorthrows - Legitimate paths like
agents.list[0].workspacestill 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.prototypevia bracket-access traversal - Fix is at the right layer (input validation in
setConfigValuebefore 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_KEYSas a frozenSetat module scope is clean
5ef98d7 to
deb4a1d
Compare
|
Updated per review feedback:
|
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
nemoclaw/src/commands/migration-state.test.tsnemoclaw/src/commands/migration-state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- nemoclaw/src/commands/migration-state.ts
|
@cv Following up on the fix above, added regression tests per ask |
…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>
deb4a1d to
7e1ee6f
Compare
|
Rebased onto current main and addressed all feedback:
|
There was a problem hiding this comment.
🧹 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
📒 Files selected for processing (2)
nemoclaw/src/commands/migration-state.test.tsnemoclaw/src/commands/migration-state.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- nemoclaw/src/commands/migration-state.ts
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>
|
Thanks for the fix! I pushed a small commit ( One last thing — do you see the Once that's green we're good to approve and merge. Thanks for the solid root-cause analysis on this one! |
|
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. |
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
PSIRT process is ideal, than you for doing that - that team will take things from there. |
Tests were added in subsequent commits (including by cv himself in 07bc4a4). Dismissing stale changes-requested review.
ericksoa
left a comment
There was a problem hiding this comment.
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.
Security Finding: Prototype Pollution via Unsanitized Config Path
Severity: MEDIUM
Reported by: FailSafe Security Researcher
Component:
nemoclaw/src/commands/migration-state.ts—setConfigValue()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, orprototype, allowing an attacker to polluteObject.prototypethrough a crafted snapshot manifest.The
configPathvalue originates frommanifest.externalRoots[].bindings[].configPathinside the snapshot'ssnapshot.jsonfile. When an operator restores a snapshot from an untrusted or compromised source, the manifest-controlledconfigPathis passed directly tosetConfigValue()without filtering.Exploitation path: A crafted
snapshot.jsonwith a binding containingconfigPath: "__proto__.isAdmin"would causesetConfigValue()to traverse intoObject.prototypeand setisAdminon 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
configPathvalues still worksconfigPath: "__proto__.isAdmin"throwsUnsafe config path segmentconfigPath: "constructor.polluted"is rejectedconfigPath: "foo.prototype.bar"is rejectedSummary by CodeRabbit
Signed-off-by: Joshua Medvinsky joshuam@ethlas.com