Skip to content

[Fix] Block memory extra path symlink traversal#80331

Merged
steipete merged 3 commits into
openclaw:mainfrom
samzong:fix/memory-extra-paths-symlink-traversal
May 11, 2026
Merged

[Fix] Block memory extra path symlink traversal#80331
steipete merged 3 commits into
openclaw:mainfrom
samzong:fix/memory-extra-paths-symlink-traversal

Conversation

@samzong

@samzong samzong commented May 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Describe the problem and fix in 2–5 bullets:

If this PR fixes a plugin beta-release blocker, title it fix(<plugin-id>): beta blocker - <summary> and link the matching Beta blocker: <plugin-name> - <summary> issue labeled beta-blocker. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.

  • Problem: memory_get authorized extraPaths directory reads with lexical containment, so symlinked intermediate components could redirect reads outside the configured memory corpus.
  • Why it matters: a configured extra memory root must not allow path traversal through attacker-controlled symlinks.
  • What changed: directory-root authorization now rejects symlinked path components, verifies realpath containment, preserves missing-file empty results, and adds focused regression coverage.
  • What did NOT change (scope boundary): this does not redesign the lower-level file-open primitive for full active-race/path-pinning semantics.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes N/A
  • Related N/A
  • This PR fixes a bug or regression

Real behavior proof (required for external PRs)

External contributors must show after-fix evidence from a real OpenClaw setup. Unit tests, mocks, lint, typechecks, snapshots, and CI are supplemental only. Screenshots are encouraged even for CLI, console, text, or log changes; terminal screenshots and copied live output count. Be mindful of private information like IP addresses, API keys, phone numbers, non-public endpoints, or other private details when providing evidence.

  • Behavior or issue addressed: readMemoryFile / memory_get extra path reads through symlinked directory components.
  • Real environment tested: local macOS worktree, Node 22 repo runtime, OpenClaw test wrapper.
  • Exact steps or command run after this patch: pnpm test packages/memory-host-sdk/src/host/read-file.test.ts extensions/memory-core/src/memory/manager.read-file.test.ts -- --reporter=verbose.
  • Evidence after fix (screenshot, recording, terminal capture, console output, redacted runtime log, linked artifact, or copied live output): copied terminal output showed 2/2 read-file.test.ts tests passing and 13/13 manager.read-file.test.ts tests passing.
  • Observed result after fix: normal extra path reads still work, missing extra path .md returns empty text, symlinked directory component reads reject with path required.
  • What was not tested: full active-race replacement during the final file open.
  • Before evidence (optional but encouraged): local repro before the fix returned outside content through extra/link/private.md.

Root Cause (if applicable)

For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write N/A. If the cause is unclear, write Unknown.

  • Root cause: the extraPaths directory branch used lexical isPathInside(additionalPath, absPath) and only lstated the leaf path, so an intermediate symlink could escape the configured root.
  • Missing detection / guardrail: there was coverage for a leaf symlink but not for symlinked directory components or missing files under extra roots.
  • Contributing context (if known): readRegularFile protects regular-file identity and leaf symlinks, but it does not authorize the whole path against the configured memory corpus root.

Regression Test Plan (if applicable)

For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write N/A.

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: packages/memory-host-sdk/src/host/read-file.test.ts.
  • Scenario the test should lock in: extra path directory reads reject symlinked components, including symlinks pointing outside and back inside the extra root; missing files under valid extra roots still return empty text.
  • Why this is the smallest reliable guardrail: it directly exercises readMemoryFile, the authorization seam used by memory_get.
  • Existing test that already covers this (if any): extensions/memory-core/src/memory/manager.read-file.test.ts covered leaf symlink rejection and general read behavior.
  • If no new test is added, why not: new tests are added.

User-visible / Behavior Changes

memory_get rejects extra path requests that traverse symlinked directory components instead of reading through them.

Diagram (if applicable)

For UI changes or non-trivial logic flows, include a small ASCII diagram reviewers can scan quickly. Otherwise write N/A.

Before:
[memory_get extra/link/private.md] -> [lexical containment passes] -> [outside file read]

After:
[memory_get extra/link/private.md] -> [symlink component rejected] -> [path required]

Security Impact (required)

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): No
  • New/changed network calls? (Yes/No): No
  • Command/tool execution surface changed? (Yes/No): No
  • Data access scope changed? (Yes/No): Yes
  • If any Yes, explain risk + mitigation: data access is narrowed for configured memory extra paths by rejecting symlink traversal through directory components.

Repro + Verification

Environment

  • OS: macOS local worktree
  • Runtime/container: Node 22 repo runtime via pnpm
  • Model/provider: N/A
  • Integration/channel (if any): memory host SDK / memory-core tests
  • Relevant config (redacted): extraPaths configured to a temporary directory in tests

Steps

  1. Create an extra memory root with inside.md.
  2. Create extra/link as a directory symlink.
  3. Request readMemoryFile for normal, missing, and symlinked paths.

Expected

  • Normal extra path file reads succeed.
  • Missing .md under a valid extra root returns empty text.
  • Symlinked directory component paths reject with path required.

Actual

  • Matches expected after this patch.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: symlink escape rejection, symlink-to-inside rejection, missing extra path empty result, existing memory-core read behavior.
  • Edge cases checked: symlink creation permission fallback for EPERM / EACCES; format, lint, and package test typecheck.
  • What you did not verify: full active-race/path-pinning behavior beyond static symlink traversal.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes/No): Yes
  • Config/env changes? (Yes/No): No
  • Migration needed? (Yes/No): No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: lower-level active race/path replacement is not fully redesigned here.
    • Mitigation: this patch blocks static symlink traversal and records the broader race-proofing as out of scope for a lower-level primitive.

Considered and deferred

  • packages/memory-host-sdk/src/host/read-file.ts:77 [BOT-SCOPE]: Fully race-proof parent traversal would need a lower-level pinned/openat-style primitive; this diff fixes static symlink traversal and rejects symlink components before read.

@openclaw-barnacle openclaw-barnacle Bot added size: S proof: supplied External PR includes structured after-fix real behavior proof. labels May 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 15ec97d415

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/memory-host-sdk/src/host/read-file.ts
@clawsweeper

clawsweeper Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The PR hardens memory extraPaths directory reads with symlink-parent and realpath authorization checks, treats ENOTDIR as a missing-file condition, and adds focused readMemoryFile regression tests.

Reproducibility: yes. source-reproducible: current main lexically authorizes extraPaths directory reads and only checks the final path, so an intermediate directory symlink can redirect the later stat/read path. I did not run tests because this review is read-only.

Real behavior proof
Needs real behavior proof before merge: The PR body only reports targeted unit-test execution and summarized pass counts; it still needs redacted after-fix runtime proof, and updating the PR body should trigger ClawSweeper re-review or a maintainer can comment @clawsweeper re-review. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, ask a maintainer to comment @clawsweeper re-review.

Next step before merge
Human follow-up is needed for contributor-supplied real behavior proof and for choosing how to handle the related broader symlink-hardening PR; there is no narrow ClawSweeper repair task left from this review.

Security
Cleared: The diff narrows memory extra-path read scope and adds no dependencies, workflow changes, secrets handling, network calls, or execution hooks.

Review details

Best possible solution:

Land the focused extraPaths static symlink traversal fix after real behavior proof is supplied and the overlap with the broader memory_get symlink PR is reconciled.

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

Yes, source-reproducible: current main lexically authorizes extraPaths directory reads and only checks the final path, so an intermediate directory symlink can redirect the later stat/read path. I did not run tests because this review is read-only.

Is this the best way to solve the issue?

Yes for the code direction: the latest patch puts the guard at the extraPaths authorization seam and uses the pinned fs-safe symlink and realpath helpers. It is not merge-ready until the external real behavior proof gate is satisfied.

Acceptance criteria:

  • pnpm test packages/memory-host-sdk/src/host/read-file.test.ts extensions/memory-core/src/memory/manager.read-file.test.ts -- --reporter=verbose

What I checked:

  • Current main vulnerable path: Current main authorizes extraPaths directory reads with lexical isPathInside(additionalPath, absPath), then only lstat()s the final path before statRegularFile/readRegularFile, leaving intermediate directory symlinks unchecked. (packages/memory-host-sdk/src/host/read-file.ts:52, 94b43127d0c0)
  • Latest PR hardening: The latest patch adds isAllowedAdditionalDirectoryPath(), calls assertNoSymlinkParents(), checks isPathInsideWithRealpath(), and switches the extraPaths directory branch to that helper. (packages/memory-host-sdk/src/host/read-file.ts:22, 588b68805a3a)
  • Prior review finding addressed: The second PR commit broadens isFileMissingError to ENOTDIR and adds a non-directory-parent regression test, matching the prior bot review's requested fix. (packages/memory-host-sdk/src/host/fs-utils.ts:18, 588b68805a3a)
  • Dependency contract checked: The pinned @openclaw/fs-safe commit treats ENOENT and ENOTDIR as not-found path errors, assertNoSymlinkParents rejects symlinked path components, and statRegularFile returns missing for not-found path errors. (package.json:1735, c7ccb99d3058)
  • Real behavior proof gate: The PR body reports a pnpm unit-test command and summarized pass counts, but does not attach redacted terminal output, logs, screenshot, recording, or runtime memory_get/readMemoryFile proof from an after-fix setup. (588b68805a3a)
  • Related open work: GitHub search found fix(memory): block symlink escapes in memory_get #68597, an open broader symlink-hardening PR for memory_get; this focused PR is still useful but maintainers should reconcile the overlap before landing both.

Likely related people:

  • @steipete: Git blame and file history show the current readMemoryFile, fs-utils, and adjacent memory read-test surfaces were recently introduced/refactored under commit 7ef587b and related memory host work. (role: recent area contributor; confidence: high; commits: 7ef587b26474, eebce9e9c7cb, bd6c7969ea9c; files: packages/memory-host-sdk/src/host/read-file.ts, packages/memory-host-sdk/src/host/fs-utils.ts, extensions/memory-core/src/memory/manager.read-file.test.ts)
  • @Takhoffman: The related merged context-window PR changed bounded memory_get/readMemoryFile behavior and adjacent tests, so this route is relevant for contract review even though the current line blame is elsewhere. (role: recent memory_get contract contributor; confidence: medium; commits: 4f00b769251d; files: packages/memory-host-sdk/src/host/read-file.ts, extensions/memory-core/src/memory/manager.read-file.test.ts)

Remaining risk / open question:

  • External proof remains test-only; the contributor should add redacted terminal output, logs, screenshot, recording, or copied live output from an after-fix runtime memory_get/readMemoryFile scenario.
  • A parallel open PR, fix(memory): block symlink escapes in memory_get #68597, covers broader memory_get symlink hardening, so maintainers should reconcile overlap before landing both.
  • Full active race/path-pinning semantics remain intentionally out of scope for this patch.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 94b43127d0c0.

@samzong samzong force-pushed the fix/memory-extra-paths-symlink-traversal branch from 15ec97d to 112dfdf Compare May 10, 2026 16:20
@samzong

samzong commented May 10, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

samzong and others added 3 commits May 11, 2026 12:27
## Considered and deferred

- packages/memory-host-sdk/src/host/read-file.ts:77 [BOT-SCOPE]: Fully race-proof parent traversal would need a lower-level pinned/openat-style primitive; this diff fixes static symlink traversal and rejects symlink components before read.
Signed-off-by: samzong <samzong.lu@gmail.com>
@steipete steipete force-pushed the fix/memory-extra-paths-symlink-traversal branch from 588b688 to 6a9c3e2 Compare May 11, 2026 11:30
@steipete steipete merged commit 1a7efca into openclaw:main May 11, 2026
110 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

Proof:

  • Local: pnpm test packages/memory-host-sdk/src/host/read-file.test.ts extensions/memory-core/src/memory/manager.read-file.test.ts -- --reporter=verbose passed 2 shards / 15 tests.
  • Testbox: pnpm check:changed passed on Blacksmith Testbox tbx_01krbcqh26ky13kbzadf7y4538 (run https://github.com/openclaw/openclaw/actions/runs/25667363296).
  • PR CI: all checks passed on pushed head 6a9c3e21531c91c3f996def9ecf7f6a566cb02eb, including check-test-types, Critical Quality (memory-runtime-boundary), CodeQL security shards, and Real behavior proof.

Landed commits:

Thanks @samzong!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

proof: supplied External PR includes structured after-fix real behavior proof. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants