Skip to content

Fix path traversal in Doctor evidence-snippet capture - #115

Open
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/doctor-evidence-path-traversal
Open

Fix path traversal in Doctor evidence-snippet capture#115
jay79-boop wants to merge 1 commit into
chrysb:mainfrom
jay79-boop:fix/doctor-evidence-path-traversal

Conversation

@jay79-boop

Copy link
Copy Markdown

Summary

readFileSnippet() in lib/server/doctor/service.js joined the workspace root with an evidence item's path field via plain path.join() and read whatever that resolved to, with no check that the result stayed inside the workspace:

const fullPath = path.join(rootDir, String(relativePath || ""));
const content = fs.readFileSync(fullPath, "utf-8");

Evidence paths aren't trusted input — they come from Doctor findings, which are either AI-generated (openclaw doctor's own analysis output) or directly user-submitted via POST /api/doctor/import's rawOutput body. normalizeEvidenceItem (lib/server/doctor/normalize.js) only trims the path string; it never validates that it's relative-and-contained.

A finding with:

{ "evidence": [{ "type": "path", "path": "../../../../etc/passwd", "startLine": 1, "endLine": 50 }] }

would have AlphaClaw read that arbitrary host file and store its contents in the doctor card's evidence.snippet, which then surfaces to the operator via the Setup UI (and potentially further, through whatever delivery channel a "request fix" call is configured to use). This bypasses the workspace-sandbox boundary the Browse routes already enforce carefully elsewhere in this codebase (routes/browse/path-utils.js's resolveSafePath) — Doctor's evidence capture just never applied the same guard.

Since Doctor findings can originate from the AI's own analysis of the repo, this isn't only reachable via the authenticated /api/doctor/import endpoint — a prompt-injected or otherwise manipulated openclaw doctor run could in principle produce a finding whose evidence path walks out of the workspace.

Fix

Reused resolveSafePath — the same traversal guard already proven correct for the file browser — so any evidence path that resolves outside workspaceRoot is silently skipped (no snippet attached) instead of read.

Test plan

  • Added a test in tests/server/doctor-service.test.js that plants a "secret" file outside the workspace, submits a finding whose evidence path traverses out to it via /api/doctor/import (using importDoctorResult), and asserts no snippet is captured for it — while a normal in-workspace evidence path (same startLine/endLine shape) still gets its snippet.
  • Full existing doctor-service, doctor-db, doctor-normalize, doctor-prompt, and routes-doctor suites pass unchanged.

readFileSnippet() joined the workspace root with an evidence item's
`path` field via plain path.join() and read whatever that resolved to,
with no check that the result stayed inside the workspace. Evidence
paths aren't trusted input -- they come from Doctor findings, which are
either AI-generated (openclaw doctor's own analysis output) or directly
user-submitted via POST /api/doctor/import's `rawOutput` body.

A finding with `evidence: [{ type: "path", path: "../../../../etc/passwd",
startLine: 1, endLine: 50 }]` would have AlphaClaw read that arbitrary
host file and store its contents in the doctor card's evidence.snippet,
which then surfaces to the operator via the Setup UI (and potentially
further, through whatever delivery channel a "request fix" call is
configured to use). This bypasses the workspace sandbox boundary the
Browse routes already enforce carefully elsewhere in this codebase
(routes/browse/path-utils.js's resolveSafePath) -- Doctor's evidence
capture just never applied the same guard.

Reused resolveSafePath (the same traversal guard already proven correct
for the file browser) so any evidence path that resolves outside
workspaceRoot is silently skipped (no snippet attached) instead of read.

Added a test that plants a "secret" file outside the workspace, submits
a finding whose evidence path traverses out to it via /api/doctor/import,
and asserts no snippet is captured for it -- while a normal in-workspace
evidence path (with the same startLine/endLine shape) still works.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant