feat: redact absolute local paths from server responses by default - #97
Open
lyeith wants to merge 1 commit into
Open
feat: redact absolute local paths from server responses by default#97lyeith wants to merge 1 commit into
lyeith wants to merge 1 commit into
Conversation
Every tool result, and both authenticated diagnostics endpoints, currently carry the operator's real filesystem layout to the connector client: the workspace root, the home directory and the Codex data directory appear verbatim in structuredContent, in text content blocks, in GET /healthz and in GET /admin/profile. For a connector reached over a tunnel that is a standing disclosure of the host's directory structure to a third party, for no functional benefit. Absolute local paths are now rewritten to stable labels before a response leaves the server: /home/you/Projects/api/src/x.ts -> [workspace:ws_1a2b.../src/x.ts] /home/you -> [home] Labels apply longest-match-first, so a workspace nested inside the home directory is labelled [workspace:...] rather than [home]/..., and only at a path boundary, so /home/you/.codex never rewrites the unrelated /home/you/.codexpro into "[codex-data]pro". Workspace roots are discovered from the payload because workspace ids are assigned at runtime; the default root, allowed roots and the Codex directory come from config. Workspace-relative paths are untouched, so tool arguments and results still round-trip: read(path: "src/x.ts") is unaffected, as is file content and command output. The two diagnostics endpoints get one extra rule: they echo *stored* profile content, which can hold paths from an older profile the running config knows nothing about, so a string that is still a bare absolute path after labelling collapses to [path]. The pattern is anchored at both ends, leaving URLs, already-labelled paths and prose containing a slash alone. It is deliberately not applied to tool results, where file content must survive intact. Redacting GET /admin/profile is safe for the admin UI: the onboarding form is rendered server-side from profileValues(), and the page's only fetch is the POST save, so no labelled value can round-trip into a stored profile. CODEXPRO_EXPOSE_ABSOLUTE_PATHS=1 restores the previous behaviour for local debugging, and server_config reports which mode is active. The smoke harnesses realpath and stat the paths CodexPro returns, so they set it explicitly at the top of each file rather than having their assertions rewritten. New coverage: scripts/smoke.mjs asserts five tool results contain no absolute path, that the workspace root equals its label, that relative paths and file content survive, and that the escape hatch still returns a real root. scripts/http-smoke.mjs runs a server with redaction on and asserts both endpoints are free of every local path, that no label swallowed part of a path segment, and that healthz still reports labelled roots and working diagnostics. npm run build and the full smoke suite pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What leaks today
Every response CodexPro sends carries the operator's real filesystem layout — including their username — to the connector client.
MCP tool results.
structuredContentand text content blocks echo absolute paths verbatim:{ "workspace_id": "ws_1a2b...", "root": "/home/alice/Projects/api", "path": "src/x.ts" }The authenticated HTTP diagnostics endpoints.
GET /healthzandGET /admin/profilebypass the tool-result path entirely, and both echo config. They are reachable with the same bearer token an MCP client already holds, so they are part of the same client-visible surface. On a live deployment I measured 4 occurrences of/home/<user>in/healthz(defaultRoot,allowedRoots) and 7 in/admin/profile(profile_path,effective.codexDir,runtime.defaultRoot, …).For a connector reached over a tunnel this is a standing disclosure of the host's directory structure — and the account name — to a third party, for no functional benefit. The model never needs the absolute prefix: every tool already addresses files workspace-relative.
The redaction scheme
Absolute local paths are rewritten to stable labels before a response leaves the server:
Labels on this branch are
[workspace:<id>](discovered from the payload, because workspace ids are assigned at runtime and appear nowhere in config),[workspace],[allowed-root],[codex-data]and[home].Two properties matter for correctness:
Longest-match-first. A workspace nested inside the home directory must be labelled
[workspace:...], not[home]/Projects/api. Entries are sorted by descending path length before substitution.Path-boundary-aware matching. This one bit me in deployment and is worth calling out, because the naive implementation looks completely fine in testing. A plain substring replace produces:
~/.codexis a string prefix of~/.codexpro, so the Codex-data label swallowed part of an unrelated segment. Not a leak, but it misreports which directory a path belongs to. A registered path now only matches when followed by a path separator or end of string, so~/.codexno longer touches~/.codexproor~/.codex-backup; those fall through to[home]as intended.One extra rule for the two diagnostics endpoints. They echo stored profile content, which can contain paths from an older saved profile that the running config knows nothing about — so config-derived labels cannot cover them by construction. Any string that is still a bare absolute path after labelling collapses to
[path]. The pattern is anchored at both ends, leaving URLs (https://host/p), already-labelled paths ([home]/.codex) and prose containing a slash untouched. This fallback is deliberately not applied to tool results, where file content and command output must survive intact.Redacting
GET /admin/profileis safe for the admin UI, which I verified before changing it: the onboarding form is rendered server-side fromprofileValues(), and the page's onlyfetchis the POST save. No labelled value can round-trip into a stored profile.What is not touched
Workspace-relative paths, so tool arguments and results still round-trip —
read(path: "src/x.ts")is unaffected, as is file content, command output and diffs.Escape hatch
CODEXPRO_EXPOSE_ABSOLUTE_PATHS=1restores the previous behaviour for local debugging, andserver_configreports which mode is active.Default: ON — and the honest cost
I've defaulted this to ON, on the grounds that leaking the host layout should be opt-in rather than opt-out, and that a connector talking to a hosted model is the common case.
The visible cost is that 13 smoke harnesses need
CODEXPRO_EXPOSE_ABSOLUTE_PATHS = '1'at the top of the file, because theyrealpath()andstat()the paths CodexPro returns. I chose a one-line explicit opt-out per harness over rewriting their assertions, so existing coverage keeps testing exactly what it tested before. That is 13 of the 18 changed files, and it is the main thing to weigh if you'd rather this defaulted to OFF — the code supports either, and flipping the default is a one-word change inconfig.ts.Verification
npm run buildand all 12 smoke scripts pass.New coverage, both written to fail against the unpatched code:
scripts/smoke.mjsruns a client with redaction on and asserts five tool results contain no absolute path, that the workspace root equals its label, that relative paths and file content survive, and that the escape hatch still returns a real root.scripts/http-smoke.mjsruns a server with redaction on and asserts both endpoints are free of every local path, thathealthzstill reports labelled roots and working diagnostics, and that no label swallowed part of a path segment. It builds that server with the layout that exposed the prefix bug (<h>/.codexas the Codex dir,<h>/.codexproasCODEXPRO_HOME); against the pre-fix code it fails with:The leak assertion names the offending JSON path rather than only the value, which is what turned the original finding into a quick diagnosis.
Beyond the test suite, this is running on a real deployment reached over a private network. Scanning the client-visible surface there —
tools/list,list_projects,open_workspace,read,tree,server_config,/healthz,/admin/profile— returns zero absolute paths and zero malformed labels, where/healthzand/admin/profilepreviously returned 4 and 7.🤖 Generated with Claude Code