Skip to content

fix(doc): deny symlink escapes on /api/doc reads - #1437

Open
bendrucker wants to merge 2 commits into
backnotprop:mainfrom
bendrucker:doc-containment-gate
Open

fix(doc): deny symlink escapes on /api/doc reads#1437
bendrucker wants to merge 2 commits into
backnotprop:mainfrom
bendrucker:doc-containment-gate

Conversation

@bendrucker

Copy link
Copy Markdown

Consolidates file handling for the review UIs into one shared module. Fixes symlinks breaking project folder containment and reading arbitrary paths on disk.

Approach

Resolving a path and reading it are now separate steps, so the permission check can run between them.

Resolving may stat a path to see whether it exists. It now checks the realpath and requires that to be inside the project in addition to the input path.

realpath fails on a path that does not exist, and the check still has to judge those, since an outside path must be refused whether or not the file is there. So it resolves the deepest part of the path that does exist and re-attaches the rest.

That catches a missing file under a symlinked folder without turning an ordinary missing file into a denial. Anything unexpected back from realpath denies the request.

Behavior Changes

  • A symlink pointing outside the project returns 403, on every path through the endpoint, on both servers.
  • HTML over 2MB returns 413. It used to be served in full when you did not pass a base.
  • Point the project root at a symlink and requests now work whether you name the link or the real folder. Both sides have to be normalized, because macOS tmpdir() is itself a symlink and normalizing one side breaks every test on a Mac.
  • A base directory that reaches outside the project through a symlink is also refused.
  • A file that exists but cannot be opened returns 500 instead of 404, because the existence check now happens before the read.

Tests

Each way a symlink can escape has its own test on both servers, since a single test covering one of them passes against a fix that missed a branch. Also covers a project root named through a symlink, an outside path that does not exist, and the HTML size limit.

`/api/doc` checked containment lexically only, so a symlink planted
inside an allowed root resolved to an in-root path and was then read
through to its target. All six content vectors leaked, plus the
existence oracle on `/api/doc/exists`.

Authorization was spread across 11 call sites in four branches of
`handleDoc`, each interleaving resolve, authorize and read, and
`apps/pi-extension/server/reference.ts` is a hand-maintained clone
carrying the same sites and the same hole.

Split resolve from read so authorization sits on one seam, in a shared
module both runtimes consume. Resolution may stat but never reads, and
the four render branches collapse to a document reader and a code
reader, which also erases the missing size cap on the HTML branch
rather than fixing it twice.

Deliberate behavior changes: escaping symlinks are denied on every
branch in both runtimes; the HTML branch enforces the 2MB cap; roots
are realpath-normalized so a root reachable through a symlink resolves
under either spelling.

Claude-Session: https://claude.ai/code/session_01HcwNnm6LtBMewU4BzNiRnM
Drop prose that restates the adjacent code and keep the facts it carried:
that resolveDocTarget's result is unauthorized, and why the annotate version
endpoints derive their history slug from the contained path.

Claude-Session: https://claude.ai/code/session_01HcwNnm6LtBMewU4BzNiRnM
@backnotprop

Copy link
Copy Markdown
Owner

Heads up while you are actively on this: the branch is conflicting with main, which moved substantially today (a large header rework in packages/editor and packages/review-editor, plus new code-nav endpoints in both server runtimes), and the conflict is blocking CI from running on your latest commits. A rebase onto current main will unblock checks. The doc-read path you are hardening was not restructured, so the rebase should be mostly mechanical. Once it is green we will get you a full review promptly; this fix is wanted.

@bendrucker

Copy link
Copy Markdown
Author

No problem will rebase now!

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.

2 participants