-
Notifications
You must be signed in to change notification settings - Fork 3
plat 443
Status: fixed on main; not deployed. Reproduced in an isolated local worktree on main f38701175.
Priority: P1.
Related: PLAT-432, PLAT-441, PLAT-118.
step_based_workflow/scripted_route_tools.go:readScriptedRouteResult uses
os.Stat and os.ReadFile directly in the agent server. Both follow symlinks.
A script can create STEP_OUTPUT_DIR/route_result.json as a link to another
server-readable JSON file. The server reads and returns that target even though
it is outside the script's assigned output directory. Child-process sandbox
restrictions do not constrain this parent-process read. This also affects named
script tools in ordinary workflows.
The comment claiming the path is the route's own output folder does not establish where the file resolves. The stat-before-read size check also does not bound the read if the file grows or changes between calls.
A temporary Go reproduction ran beside the existing step tests:
- Write a fake
{"fixture_secret":"outside-output-dir"}JSON file outside an assigned route-output directory. - Symlink
route-output/route_result.jsonto that file. - Call
readScriptedRouteResult(route-output). - It returns the external JSON with no error.
No real credentials or production files were accessed. This reproduces the unconfined server read, not a live production sandbox exploit. Existing focused Relay/route/strict-script tests and server ingress tests pass separately.
Reuse the platform's confined workspace-file reader, enforcing the route's allowed output path, symlink confinement and a bounded read. Include a regression for a leaf symlink and a symlinked ancestor, with a regular JSON file still returned successfully. Do not deploy the affected tool path as ready until this boundary is fixed and verified.
readScriptedRouteResult(confineRoot, outputDir): resolves the workflow folder and
the output folder with EvalSymlinks and refuses an output folder that resolves
outside the workflow (a symlinked ancestor); Lstats the file and accepts only a
regular file (a leaf symlink, even to another file inside the workflow, a
directory or any special file is refused); opens it and requires os.SameFile
with the checked file; reads through a LimitReader of the cap plus one byte, so
growth between check and read cannot exceed the bound. An unusable file is logged
and the run summary is kept, as before. Tests: regular file returned, leaf
symlink outside, link to another workflow file, symlinked ancestor outside, a
directory named route_result.json.
Not done: this is a purpose-built reader, not a shared confined workspace-file
reader (none exists in the codebase); consolidating the several EvalSymlinks
checks into one helper is a separate cleanup.
Auto-synced from docs/ on main. Edit there, not here.