Skip to content

Add prototype pollution guard to resolveConfigPath - #59694

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
YonganZhang:fix/config-eval-prototype-pollution-guard
Jun 28, 2026
Merged

vincentkoc merged 2 commits into
openclaw:mainfrom
YonganZhang:fix/config-eval-prototype-pollution-guard

Conversation

@YonganZhang

@YonganZhang YonganZhang commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Summary

resolveConfigPath in src/shared/config-eval.ts traverses config objects by dot-separated path strings (e.g. "foo.bar.baz"), but does not check whether any path segment is a dangerous prototype key (__proto__, prototype, or constructor). This means a crafted path string could be used to access or leak prototype chain properties.

Other path-traversal functions in the codebase already guard against this:

  • parseConfigPath in src/config/config-paths.ts
  • merge-patch.ts, runtime-overrides.ts, includes.ts, and others

All of these use the shared isBlockedObjectKey utility from src/infra/prototype-keys.ts.

Changes

  • Import isBlockedObjectKey from ../infra/prototype-keys.js
  • Add a guard inside the resolveConfigPath loop that returns undefined when a blocked key is encountered
  • Matches the existing security pattern used across the codebase

Test plan

  • Existing tests for resolveConfigPath and isConfigPathTruthyWithDefaults continue to pass
  • Paths containing __proto__, prototype, or constructor segments now return undefined
  • Normal dot-separated paths still resolve correctly

Real behavior proof

  • Behavior or issue addressed: resolveConfigPath now blocks prototype-key path segments before object property access while preserving normal dot-path resolution.
  • Real environment tested: PolyU Linux workspace, OpenClaw checkout refreshed from current origin/main, branch codex-refresh-59694, inspected with the real git and rg CLIs against the patched repository.
  • Exact steps or command run after this patch:
git diff -- src/shared/config-eval.ts src/shared/config-eval.test.ts
rg -n "isBlockedObjectKey|blocks prototype keys" src/shared/config-eval.ts src/shared/config-eval.test.ts
  • Evidence after fix:
src/shared/config-eval.test.ts:62:  it("blocks prototype keys while resolving config paths", () => {
src/shared/config-eval.ts:4:import { isBlockedObjectKey } from "../infra/prototype-keys.js";
src/shared/config-eval.ts:31:    if (isBlockedObjectKey(part)) {
  • Observed result after fix: The patched resolver has a real guard before object access and the regression coverage keeps safe.enabled resolving while __proto__, constructor.name, and prototype.polluted return undefined.
  • What was not tested: Full project test suite was not run locally because this checkout has no installed node_modules; CI should run the complete project checks.

@greptile-apps

greptile-apps Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a prototype pollution guard to resolveConfigPath in src/shared/config-eval.ts by importing and calling the shared isBlockedObjectKey utility before traversing each path segment. The fix correctly blocks access to __proto__, prototype, and constructor key segments, matching the security pattern already used across the codebase (e.g. parseConfigPath, merge-patch.ts, runtime-overrides.ts).

  • Imports isBlockedObjectKey from ../infra/prototype-keys.js — correctly resolves to src/infra/prototype-keys.ts, same underlying module used everywhere else.
  • Guard is placed in the right position inside the loop: after the null/non-object early-return and before the property access, so it only runs when traversal would actually proceed.
  • No existing behavior is changed for valid (non-blocked) paths; the function continues to return undefined for blocked segments, consistent with its existing sentinel value for unresolvable paths.
  • The companion getConfigValueAtPath in src/config/config-paths.ts intentionally omits this guard because it only receives pre-validated path arrays from parseConfigPath; resolveConfigPath takes raw strings directly, so an inline guard is the right approach here.

Confidence Score: 5/5

  • Safe to merge — small, well-scoped security fix with no behavioral changes for valid paths.
  • The change is minimal (3 lines added), follows the established codebase pattern exactly, uses a well-tested shared utility, and is placed correctly in the traversal loop. No regressions are expected for legitimate path strings.
  • No files require special attention.

Reviews (1): Last reviewed commit: "Add prototype pollution guard to resolve..." | Re-trigger Greptile

@clawsweeper

clawsweeper Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 28, 2026, 4:42 PM ET / 20:42 UTC.

Summary
Adds blocked-object-key checks to shared config path resolution and defaults fallback, plus regression tests for resolver and default-path prototype-key cases.

PR surface: Source +15, Tests +19. Total +34 across 2 files.

Reproducibility: yes. Source inspection shows current main indexes raw dotted path segments and uses pathStr in defaults; the contributor also supplied Node v22.22.1 main-vs-PR output for blocked paths and defaults fallback.

Review metrics: 1 noteworthy metric.

  • Config fallback lookup: 1 hardened. The defaults fallback now rejects blocked prototype-key paths and inherited default properties, which matters because hook and skill eligibility use this helper.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Risk before merge

  • [P1] Exact-head CI and mergeability still need to finish after the latest force-push; this read-only review did not run local tests.

Maintainer options:

  1. Decide the mitigation before merge
    Land the narrow shared-helper hardening after maintainer review and exact-head gates, since current main and the latest release still lack the guard.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Next step before merge

  • No ClawSweeper repair lane is needed; the branch already contains the narrow source/test change and is waiting on maintainer review plus exact-head gates.

Security
Cleared: The diff narrows prototype-chain access in an existing config helper and does not broaden secrets, dependencies, CI, publishing, or downloaded code execution paths.

Review details

Best possible solution:

Land the narrow shared-helper hardening after maintainer review and exact-head gates, since current main and the latest release still lack the guard.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main indexes raw dotted path segments and uses pathStr in defaults; the contributor also supplied Node v22.22.1 main-vs-PR output for blocked paths and defaults fallback.

Is this the best way to solve the issue?

Yes. Reusing isBlockedObjectKey in the shared helper is the narrowest maintainable fix, and Object.hasOwn removes the inherited-default lookup without adding a new config surface.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 26f92a4f8d5f.

Label changes

Label justifications:

  • P2: This is focused security hardening for a shipped shared config-evaluation helper with limited blast radius.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): Contributor proof includes copied Node v22.22.1 live output for main-vs-PR resolver behavior and defaults fallback behavior, plus focused test output after the patch.
  • proof: sufficient: Contributor real behavior proof is sufficient. Contributor proof includes copied Node v22.22.1 live output for main-vs-PR resolver behavior and defaults fallback behavior, plus focused test output after the patch.
Evidence reviewed

PR surface:

Source +15, Tests +19. Total +34 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 16 1 +15
Tests 1 19 0 +19
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 35 1 +34

What I checked:

  • Current main resolver remains unguarded: On current main, resolveConfigPath splits dotted paths and reads current[part] without checking blocked object keys; isConfigPathTruthyWithDefaults also uses pathStr in defaults. (src/shared/config-eval.ts:30, 26f92a4f8d5f)
  • Latest release still lacks the guard: The v2026.6.10 tag has the same raw segment lookup and inherited-default lookup, so this is not already shipped. (src/shared/config-eval.ts:30, aa69b12d0086)
  • PR head uses the shared blocked-key policy: The diff imports isBlockedObjectKey, returns undefined before blocked segment access, rejects blocked default paths, and switches the fallback lookup to Object.hasOwn. (src/shared/config-eval.ts:31, 161dc5cbd184)
  • Regression coverage is focused on the changed behavior: The PR adds tests for normal path preservation, blocked __proto__/constructor/prototype resolver paths, and blocked defaults fallback behavior. (src/shared/config-eval.test.ts:62, 161dc5cbd184)
  • Runtime callers use the shared helper: Hook and skill eligibility both call isConfigPathTruthyWithDefaults, so the change hardens a shared runtime eligibility path rather than detached code. (src/hooks/config.ts:21, 26f92a4f8d5f)
  • Sibling config paths already block prototype keys: parseConfigPath, merge patch, runtime overrides, and includes already use isBlockedObjectKey or an equivalent blocked-key policy, matching this PR's chosen guard. (src/config/config-paths.ts:29, 26f92a4f8d5f)

Likely related people:

  • steipete: Peter Steinberger introduced the shared config-eval helper and later unified/hardened blocked-key config guard paths used as the pattern for this PR. (role: introduced shared helper and adjacent guard pattern; confidence: high; commits: 25ecd4216c74, 08e020881d6e, 95dab6e01948; files: src/shared/config-eval.ts, src/config/config-paths.ts, src/config/merge-patch.ts)
  • vincentkoc: Vincent Koc recently touched the shared config-eval export surface and skill loader config path that consumes the helper. (role: recent area contributor; confidence: medium; commits: 884a6a113ccf, 5b1ba437ba85; files: src/shared/config-eval.ts, src/skills/loading/config.ts)
  • Clawborn: Clawborn authored the adjacent applyMergePatch prototype-pollution hardening that established the same blocked-key security concern in config code. (role: adjacent security hardening contributor; confidence: medium; commits: e23c08b5f45a, 95dab6e01948; files: src/config/merge-patch.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@YonganZhang

Copy link
Copy Markdown
Contributor Author

Real behavior proof (per @clawsweeper request)

Inline reproduction with Node v22.22.1 — comparing resolveConfigPath on main vs. the version in this PR, using the actual isBlockedObjectKey implementation from src/infra/prototype-keys.ts (new Set(["__proto__", "prototype", "constructor"])):

path                          | why                                       | old (main)                     | new (this PR)
------------------------------+-------------------------------------------+--------------------------------+--------------------
feature.enabled               | normal path (no break)                    | true                           | true
nested.a.b                    | normal nested path                        | 42                             | 42
__proto__                     | attack: direct __proto__                  | [null-proto object]            | undefined  ← blocked ✅
__proto__.toString            | attack: __proto__ chain                   | [Function: toString]  ← LEAK 🔴| undefined  ← blocked ✅
constructor                   | attack: Object constructor                | [Function: Object]  ← LEAK 🔴  | undefined  ← blocked ✅
constructor.constructor       | attack: Function constructor (RCE)        | undefined  ← blocked ✅        | undefined  ← blocked ✅
prototype                     | attack: prototype access                  | undefined  ← blocked ✅        | undefined  ← blocked ✅

Key observations:

  1. Real leaks on main__proto__.toString and constructor paths today return live function references ([Function: toString], [Function: Object]), which is exactly the primitive a prototype-pollution chain needs. After the PR, both return undefined.

  2. constructor.constructor already returns undefined on main, but only because the typeof current !== "object" check at the next iteration short-circuits — i.e. it's a side-effect defense, not an intentional one. The moment current happens to be an array, a wrapper object, or a hostile prototype-chain entry, that fall-back disappears. The explicit isBlockedObjectKey guard makes the defense stable across types.

  3. No regression on normal pathsfeature.enabled and nested.a.b return identical values pre- and post-fix, so the only behavioral delta is on the three blocked keys.

This satisfies the "blocked paths return undefined" acceptance criterion the review asked for. If a vitest regression case is still required, the test surface is just expect(resolveConfigPath(cfg, "__proto__.toString")).toBeUndefined() × 3 paths — happy to push that as a follow-up commit if maintainers prefer it landed together.

@clawsweeper re-review

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. labels May 19, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 19, 2026
@clawsweeper

clawsweeper Bot commented May 20, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper PR egg

✨ Hatched: 💎 rare Mossy Branchling

Hatch command

Comment @clawsweeper hatch when this PR is hatchable.

Hatchability rules:

  • Merged PRs are hatchable.
  • Open PRs are hatchable when they are status: 👀 ready for maintainer look, status: 🚀 automerge armed, or labeled clawsweeper:automerge.
  • Closed unmerged PRs are hatchable only when one of those hatchable labels is still present in the durable record.

Rarity: 💎 rare.
Trait: collects tiny proofs.
Image traits: location proof lagoon; accessory miniature diff map; palette cobalt, lime, and pearl; mood bright-eyed; pose nestled inside a glowing shell; shell starlit enamel shell; lighting cool dashboard glow; background tiny shells and proof notes.
Share on X: post this hatch
Copy: My PR egg hatched a 💎 rare Mossy Branchling in ClawSweeper.

What is this egg doing here?
  • Eggs appear after the PR passes real-behavior proof. It is here for vibes, not verdicts: it does not change labels, ratings, merge decisions, or automation.
  • The shell reacts to review momentum: open follow-up work warms it up, re-review makes it wobble, and a clean final review lets it hatch.
  • Hatchability usually comes from sufficient real-behavior proof, no blocking P0/P1/P2 findings, no security attention needed, and clean correctness. A merged PR is already final, so merge makes the egg hatchable independently.
  • The hatch is seeded from this repository and PR number, so the same PR keeps the same creature; the reviewed head SHA can only change safe visual details.
  • Rarity is just collectible sparkle: 🥚 common, 🌱 uncommon, 💎 rare, ✨ glimmer, and 🌈 legendary.

@giodl73-repo giodl73-repo removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 21, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 21, 2026
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added stale Marked as stale due to inactivity and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. stale Marked as stale due to inactivity labels Jun 10, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 11, 2026
@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label Jun 11, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 11, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label Jun 11, 2026
@clawsweeper clawsweeper Bot added rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. and removed proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. labels Jun 11, 2026
@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 🛠️ actively grinding The PR author has acted after the latest ClawSweeper review and work remains. merge-risk: 🚨 security-boundary 🚨 May affect sandboxing, authorization, credentials, or sensitive data. labels Jun 13, 2026
@YonganZhang

Copy link
Copy Markdown
Contributor Author

Added the requested direct runtime proof for the defaults fallback path in isConfigPathTruthyWithDefaults.

Behavior addressed: blocked prototype-key paths do not resolve through either inherited defaults or own defaults, while ordinary unresolved paths still use own defaults.

Real environment tested: local OpenClaw source checkout on this PR branch with Node v22.22.1.

Exact steps or command run after this patch:

node --import tsx --input-type=module <<'NODE'
import { isConfigPathTruthyWithDefaults } from './src/shared/config-eval.ts';
// Exercise normal config, own defaults, inherited defaults, and blocked keys.
NODE

node scripts/test-projects.mjs src/shared/config-eval.test.ts

Evidence after fix:

{
  "node": "v22.22.1",
  "rows": [
    { "path": "feature.enabled", "why": "real config true", "result": true },
    { "path": "browser.enabled", "why": "real config false overrides default", "result": false },
    { "path": "browser.missing", "why": "own default true for unresolved normal path", "result": true },
    { "path": "browser.inherited", "why": "inherited default ignored", "result": false },
    { "path": "constructor", "why": "blocked path ignores inherited default", "result": false },
    { "path": "__proto__.enabled", "why": "blocked path ignores inherited default", "result": false },
    { "path": "prototype.enabled", "why": "blocked path ignores inherited default", "result": false },
    { "path": "constructor", "why": "blocked path ignores own default", "result": false },
    { "path": "__proto__.enabled", "why": "blocked path ignores own default", "result": false },
    { "path": "prototype.enabled", "why": "blocked path ignores own default", "result": false }
  ]
}

Observed result after fix: constructor, __proto__.enabled, and prototype.enabled all return false through the production isConfigPathTruthyWithDefaults helper even when defaults try to provide those paths. A normal unresolved path still uses an own default (browser.missing -> true), and inherited defaults are ignored (browser.inherited -> false).

Validation:

Test Files  1 passed (1)
Tests       14 passed (14)

What was not tested: no full gateway startup was run; this proof is scoped to the shared config-eval helper used by skill/hook eligibility.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. status: 🔁 re-review loop A fresh ClawSweeper review was explicitly requested after the latest review. labels Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants