fix(image): prevent path traversal in /api/image endpoint#865
Open
sebastiondev wants to merge 1 commit into
Open
fix(image): prevent path traversal in /api/image endpoint#865sebastiondev wants to merge 1 commit into
sebastiondev wants to merge 1 commit into
Conversation
validateImagePath() only checked file extensions but did not verify the resolved path stays within the project root or upload directory. This allowed reading arbitrary image files from anywhere on the filesystem via GET /api/image?path=<traversal>. Add isWithinAllowedRoot() containment check that ensures the resolved path is within either process.cwd() (project root) or the UPLOAD_DIR (temp uploads). Applied to both packages/server/image.ts and the duplicated handler in apps/pi-extension/server/handlers.ts.
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.
Vulnerability Summary
The
/api/imageendpoint in bothpackages/server/image.tsandapps/pi-extension/server/handlers.tsis vulnerable to path traversal (CWE-22). Thepathquery parameter is user-controlled and is resolved to an absolute filesystem path usingresolve(), but the only validation performed is an image-extension check. An attacker can supply a traversal payload (e.g.,../../../etc/hostname.svgor an absolute path like/home/user/.ssh/id_rsa.png) to read any file on the filesystem, as long as it has a recognized image extension.Affected files:
packages/server/image.ts—validateImagePath()(used byshared-handlers.tswhich serves all plan/review/annotate servers)apps/pi-extension/server/handlers.ts— duplicatevalidateImagePath()for the PI extension serversData flow:
GET /api/image?path=../../../etc/hostname.svgurl.searchParams.get("path")extracts the raw path — no sanitizationvalidateImagePath()callsresolve(rawPath)→ produces an absolute path.svgis allowed)Bun.file(resolved)/readFileSync(resolved)reads and returns the file contentsThe
basequery parameter follows the same vulnerable flow —resolvePath(base, imagePath)can also escape the project directory.Risk Assessment
Severity: Medium
The vulnerability is constrained to files with image extensions (.png, .jpg, .svg, .gif, etc.), which limits the blast radius. However:
.svgfiles are plain-text XML and can contain arbitrary content. If an SVG file exists outside the project root (config templates, exported data, etc.), its contents are fully readable.PLANNOTATOR_REMOTE=1), the server binds to0.0.0.0— making the unauthenticated/api/imageendpoint network-accessible./api/image?path=../../other-project/secret.svgrequests, reading files from the victim's filesystem.Proof of Concept
Start any Plannotator server (plan, review, or annotate). Then:
In each case, the server returns the file contents with a 200 response. Without this fix,
validateImagePath()only checks the extension — any path that resolves to an image-extension file is served regardless of location.You can verify the vulnerability by placing a test file outside the project directory:
After applying this fix, the same requests return
403 Access denied: path is outside project root.Fix Description
This PR adds an
isWithinAllowedRoot()containment check tovalidateImagePath()in both server implementations. The function:process.cwd()) and the upload temp directory (UPLOAD_DIR)/to prevent prefix-bypass attacks like/project-root-evil/...)The check is added to
validateImagePath()itself, so it protects both the primarypathparameter and the fallbackbaseparameter resolution — every code path that serves images goes through this function.Files changed:
packages/server/image.ts— addedisWithinAllowedRoot()and integrated it intovalidateImagePath()apps/pi-extension/server/handlers.ts— same change for the PI extension's copy of the validation logicpackages/server/image.test.ts— added test cases for traversal rejection (absolute paths,../sequences, paths outside project root) and updated existing tests to use project-relative pathsTest Results
The existing test suite was extended with three new path-traversal test cases. All tests pass:
accepts supported extensions within project root— image files inside the project are still servedaccepts images within UPLOAD_DIR— uploaded temp files are still accessiblerejects unsupported extensions— non-image files still rejectedrejects files with no extension— extensionless files still rejectedresolves path— relative paths resolve to absolute (existing behavior preserved)rejects path traversal outside project root—/etc/passwd.png→ 403rejects ../ traversal escaping project root—../../../etc/secret.png→ 403rejects absolute path outside project root—/tmp/outside/image.png→ 403Adversarial Review
Before submitting, we attempted to disprove this finding. We checked whether any framework-level middleware, authentication gate, or path sanitization existed upstream of
validateImagePath(). There is none — the codebase's own documentation confirms "No authentication" on all endpoints. We also considered whether the image-extension restriction alone was sufficient mitigation; it is not, because.svgfiles are plain-text XML and can contain sensitive data. Thebaseparameter could also be used to pivot the resolve root, but the fix covers that path too sincevalidateImagePath()is called on the resolved result.Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.