You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Oct 23, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Oct 23, 2026. It is now read-only.
Invariant gap: assertAllowedPath is lexical on Linux — symlink inside approved root escapes for read/write/edit #496
Coverage status:BYPASS_PATH_EXISTS · Severity: HIGH
Enforcement gap
src/roots.ts:21-57 assertAllowedPath/isPathInsideRoot compares resolve+relative lexically; realpath only for darwin/win32 case folding (:37). Sink server.ts:4121/4241/4353 → workspaces.ts:661 → pi-tools.ts:107/118/128 follows the link. roots.test.ts never tests containment through a symlink.
Minimal reproducer
Run against James3014/devspace @ e7f6397c9123ec682832fb31bdda06dd0c2e5352 (reproducers import the real production code; absolute paths assume a /home/user/<repo> checkout).
devspace/symlink_escape.mts ()
// Path containment is lexical on Linux: a symlink inside the workspace root lets// read/write file tools reach files outside every allowed root.import{mkdtempSync,mkdirSync,symlinkSync,writeFileSync,readFileSync,existsSync}from"node:fs";import{join}from"node:path";import{tmpdir}from"node:os";constSRC=process.env.DEVSPACE_SRC!;const{ readFileTool, writeFileTool }=awaitimport(`${SRC}/pi-tools.ts`);const{ assertAllowedPath }=awaitimport(`${SRC}/roots.ts`);constbase=mkdtempSync(join(tmpdir(),"ds-symlink-"));constroot=join(base,"allowed-root");constoutside=join(base,"outside-secret");mkdirSync(root);mkdirSync(outside);writeFileSync(join(outside,"auth.json"),'{"ownerToken":"TOP-SECRET"}');symlinkSync(outside,join(root,"vendor-link"),"dir");// e.g. committed in a cloned repoconstctx={ root,cwd: root};console.log("assertAllowedPath(vendor-link/auth.json) ->",assertAllowedPath(join(root,"vendor-link/auth.json"),[root]));try{assertAllowedPath(join(outside,"auth.json"),[root]);}catch(e: any){console.log("direct outside path ->",e.name);}constr=awaitreadFileTool({path: "vendor-link/auth.json"},ctx);console.log("read isError:",!!r.isError,"content:",JSON.stringify(r.content?.[0]?.text??r.content).slice(0,80));constw=awaitwriteFileTool({path: "vendor-link/planted.sh",content: "echo pwned\n"},ctx);console.log("write isError:",!!w.isError,"| file created OUTSIDE root:",existsSync(join(outside,"planted.sh")),readFileSync(join(outside,"planted.sh"),"utf8").trim());
Additionally require isPathInsideRoot(canonicalizePath(path), canonicalizePath(root)) (realpath of nearest existing ancestor) on all platforms.
Acceptance: a negative/tamper test that asserts the rejected behavior at the enforcing sink (not object shape), plus the existing positive path still passing.
Related issues
none found
Provenance
Repository-wide Invariant Enforcement Coverage Audit, 2026-10-11 (finding DS-1). Read-only audit; reproducers and the full INVARIANT_COVERAGE_MATRIX are on branch claude/nexus-invariant-coverage-audit-k9sce3 of James3014/Nexus-new under docs/audits/2026-10-11-invariant-coverage/. This Issue is a bounded finding, not an implementation authorization.
Re-ran the reproducer from this Issue body verbatim against devspace main e1caddf (2026-10-16, macOS, tsx):
assertAllowedPath(<root>/vendor-link/auth.json) (symlink to a dir outside the root) now throws AccessDeniedError; isPathInsideRoot requires canonical (realpath) containment on every platform. Regression tests: src/roots.test.ts (added in #502).
The escape the Issue reproduced is now rejected at the enforcing sink. #502 used Refs pending verification; this is that verification (Issue bundle D, worker containment).
Activity
github-actions commentedon Oct 11, 2026
Authority:
RESEARCH_OBSERVATION_ONLY / NO_ENGINEERING_AUTHORITYHybrid replication capture receipt
{ "body_gzip_base64": "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", "capture_sha256": "94d28c0d27334de8a171b375319c104b71a8b5bd2f82c8c9017ce1223ca7bce6", "captured_at": "2026-10-11T08:50:24Z", "contract_sha256": "616ba94a66a311534b63aff12843fe4e718f03a58b9f8ee155ffa716b55cbae7", "created_at": "2026-10-11T08:50:16Z", "default_branch": "main", "issue_number": 496, "issue_updated_at": "2026-10-11T08:50:16Z", "pre_implementation_revision": "e7f6397c9123ec682832fb31bdda06dd0c2e5352", "repository": "James3014/devspace", "schema": "nexus.hybrid_replication.capture.v1", "source_event_id": "actions:38126219853:1", "title": "Invariant gap: assertAllowedPath is lexical on Linux — symlink inside approved root escapes for read/write/edit" }github-actions commentedon Oct 11, 2026
Authority:
RESEARCH_OBSERVATION_ONLY / NO_ENGINEERING_AUTHORITY{ "body_gzip_base64": "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", "delta_sha256": "18ba81a2b15e0e2fbfec924296f5ab34d653304196b4493aabc18a3108ebe7e8", "edited_at": "2026-10-11T11:15:06Z", "issue_updated_at": "2026-10-11T11:15:00Z", "original_capture_sha256": "94d28c0d27334de8a171b375319c104b71a8b5bd2f82c8c9017ce1223ca7bce6", "schema": "nexus.hybrid_replication.contract_delta.v1", "source_event_id": "actions:38134866468:1", "task_key": "James3014/devspace#496", "title": "Invariant gap: assertAllowedPath is lexical on Linux — symlink inside approved root escapes for read/write/edit" }fix: close audited file-root, push-target and scope-observer gaps (#502)
james3014-nexus-agent commentedon Oct 16, 2026
Verified fixed by #502 (c22dd33) — closing
Re-ran the reproducer from this Issue body verbatim against devspace main
e1caddf(2026-10-16, macOS, tsx):assertAllowedPath(<root>/vendor-link/auth.json)(symlink to a dir outside the root) now throwsAccessDeniedError;isPathInsideRootrequires canonical (realpath) containment on every platform. Regression tests:src/roots.test.ts(added in #502).The escape the Issue reproduced is now rejected at the enforcing sink. #502 used
Refspending verification; this is that verification (Issue bundle D, worker containment).