pingdotgg / pingdotgg/t3code

[Bug]: Signed asset links for PDFs are scoped to the parent directory, exposing every sibling PDF

Open
#9,228 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
23k
Forks
5.9k
Avg merge
11h 14m
Merged PRs (30d)
357

Description

Before submitting
  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.
Area

apps/server

Steps to reproduce
  1. Put two PDFs in the same workspace folder, e.g. docs/a.pdf and docs/b.pdf.
  2. Have a thread reference docs/a.pdf, so the server mints an asset link of the form /api/assets/<token>/a.pdf.
  3. Take that link and replace only the trailing filename with b.pdf.
  4. b.pdf is served with 200, although it was never referenced in the thread.
Expected behavior

A signed asset link grants access to the file it was minted for, and to nothing the user did not reference.

Actual behavior

Token minting only produces workspace-file-exact claims when isWorkspaceImagePreviewPath(relativePath) is true, i.e. for images. Everything else — including .pdf, .htm and .html — gets workspace-file claims scoped to path.dirname(relativePath).

On serving, the request path is joined onto baseRelativePath and gated only by PREVIEW_ASSET_EXTENSIONS plus a . / .. / dotfile segment check. Every sibling or nested file with a preview extension is therefore reachable, unauthenticated, for the token's lifetime (~1h in my case), including over the public relay host.

A directory scope is the right call for HTML previews, which need to resolve sibling CSS, JS and fonts. A PDF has no sibling resources to resolve, yet it inherits that scope by sharing the browser-preview category with HTML.

To be clear about what is not affected: this is not a path traversal. ../ and %2e%2e%2f are correctly rejected with 404, and files whose extension is outside the allowlist are not served.

Practical consequence: sharing a single asset link — the natural way to hand a document to someone from a phone — silently shares every PDF sitting next to it. Folders that hold one sensitive document among its siblings (signed bank instructions, invoices, contracts) are exactly the case where a user would share one link.

A possible fix would be to mint workspace-file-exact for resources that never need sibling resolution, i.e. keep the directory-scoped claims for .htm / .html and treat .pdf like images.

Impact

Major degradation or frequent failure

Version or commit

0.0.39-nightly.20260902.1257

Environment

macOS 12.6, Node v24.15.0, server started with serve --host 127.0.0.1 --port 3773, link opened through the T3 Connect relay host.

Logs or stack traces
# token redacted; same token in all three requests, minted for <folder>/file-A.pdf

$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' "$BASE/<token>/file-A.pdf"
200 57138          # referenced file — expected

$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' "$BASE/<token>/file-B.pdf"
200 49388          # never referenced, unrelated document in the same folder

$ curl -s --path-as-is -o /dev/null -w '%{http_code}\n' "$BASE/<token>/../../../some/other/path.md"
404                # traversal correctly rejected
Workaround

Send the file itself rather than the asset link whenever the containing folder holds anything the recipient should not see.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in apps/server by locating isWorkspaceImagePreviewPath, workspace-file-exact/workspace-file claim minting, and PREVIEW_ASSET_EXTENSIONS. Reproduce the curl requests from the issue, then trace token validation and serving; done means a PDF token cannot fetch sibling PDFs while HTML previews retain required directory access, with traversal and extension checks still passing.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.