Skip to content

fix(web): fetch a DID's blobs only from a public PDS - #298

Merged
botandbang merged 1 commit into
mainfrom
bnb/recording-audio-ssrf
Oct 30, 2026
Merged

botandbang merged 1 commit into
mainfrom
bnb/recording-audio-ssrf

Conversation

@botandbang

Copy link
Copy Markdown
Contributor

What and why

The set player's audio route resolved a DJ's PDS from their DID document and fetched com.atproto.sync.getBlob from it on the server, following redirects. Anyone can publish a recording record and point their DID document (a did:web is easy) at a private or link-local address, and the site would fetch it from inside its network. fetchBlob in app.ts, used for any press kit's pictures, had the same gap. The credentials code already checked PDS addresses with assertSafeEndpoint; these two paths didn't.

Added: IdentityResolver.safePds(did), which resolves the DID and refuses a PDS that isn't https or is a private, loopback or link-local address (localhost still allowed in dev).

Changed: the audio route and fetchBlob get their PDS through safePds and fetch with redirect: 'error', so a refused PDS or a redirect ends as a 502 "Recording unavailable" with a warn log. The credentials code's resolve-then-check pair is now one safePds call, with no behaviour change.

Not changed: @atproto/identity still fetches a did:web document from its domain without an address check. That's a blind GET inside the library.

How it was tested

  • audio.test.ts: PDSes at https://127.0.0.1, https://10.0.0.1, https://169.254.169.254, https://[::1] and http://pds.example get a 502 and nothing is fetched; a public PDS still serves a 206 range; a real local server answering 302 isn't followed. Breaking the guard or the redirect rule fails them.
  • Locally: format:check, lint, check, knip, ast-grep, test (787) and test:coverage (984 passed, thresholds met).

Checklist

  • Tests cover the change
  • The READMEs, docs/ and --help still say what the code does

The set player's audio route and fetchBlob (press kits' pictures) resolved a DID's PDS
and fetched from it on the server, following redirects, so a DID document naming a
private or local address had the site fetch it. Both now get the PDS through
IdentityResolver.safePds, which refuses non-https and private addresses as the
credentials code already did, and refuse redirects.
@botandbang
botandbang requested a review from bnb as a code owner October 29, 2026 21:47
@github-actions github-actions Bot added web The Local Network website (apps/web) web: server Spaces, sync, HappyView, the database and the server around them tests Tests, fixtures and test harnesses labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Coverage Report for apps/web

Status Category Percentage Covered / Total
🟢 Lines 94.68% (🎯 93%)
🟰 ±0 pp
6453 / 6815
🟢 Statements 93.14% (🎯 91%)
🟰 ±0 pp
9912 / 10641
🟢 Functions 93.56% (🎯 92%)
🟰 ±0 pp
2530 / 2704
🟢 Branches 83.37% (🎯 80%)
⬆️ +0.02 pp
4653 / 5581
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
apps/web/src/lib/server/app.ts 69.73%
🟰 ±0 pp
46.8%
🟰 ±0 pp
57.77%
🟰 ±0 pp
76.92%
🟰 ±0 pp
80, 112, 138, 140, 143, 165, 210-223, 287, 297, 304, 307-308, 320, 342, 359-381, 385, 394-397, 407-412
apps/web/src/lib/server/atproto/credentials.ts 87.3%
⬇️ -0.10 pp
71.18%
🟰 ±0 pp
93.1%
🟰 ±0 pp
90.17%
⬇️ -0.09 pp
99, 129, 146, 153-160, 192, 230-247, 381
apps/web/src/lib/server/atproto/identity.ts 88.31%
⬆️ +0.48 pp
85.89%
⬆️ +1.28 pp
86.66%
⬆️ +0.95 pp
98.41%
⬆️ +0.08 pp
70, 87, 100, 108, 117, 120, 124, 161, 184
apps/web/src/lib/server/media/audio.ts 100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
100%
🟰 ±0 pp
Generated in workflow #559 for commit 638d518 by the Vitest Coverage Report Action

Size

Compared with main. KiB are 1,024 bytes.

This pull request main Change
Client JavaScript 635.1 KiB 635.1 KiB 0 B (0%)
Client JavaScript, brotli 209.5 KiB 209.5 KiB 0 B (0%)
Client CSS 88.6 KiB 88.6 KiB 0 B (0%)
Client CSS, brotli 12.7 KiB 12.7 KiB 0 B (0%)

@botandbang
botandbang merged commit f5fe81d into main Oct 30, 2026
50 checks passed
@botandbang
botandbang deleted the bnb/recording-audio-ssrf branch October 30, 2026 16:20
botandbang added a commit that referenced this pull request Nov 1, 2026
…etch

bnb/profiles brings in main, whose #298 added IdentityResolver.safePds:
the account's PDS once assertSafeEndpoint passed it, the same check
before a separate fetch that this branch removes. main called it from
core.fetchBlob, SpaceCredentialManager's mint, and the /media/audio
route.

Conflicts:
- app.ts fetchBlob: the URL comes from identity.resolve, and the request
  goes through identity.serviceFetch with redirect 'error'.
- credentials.ts mint: the authority's PDS from identity.resolve, as
  this branch had it.

safePds is removed. The /media/audio route used it with SvelteKit's
fetch: recordingAudio now takes the identity resolver in place of a
pds lookup and a fetch, and reads the PDS with serviceFetch, so a
caller can't hand it the plain fetch. audio.test.ts builds a real
IdentityResolver over its fake PDS.

serviceFetch drops its 60 second limit. safeFetchWrap's timeout pipes
every body through a stream that reads ahead of the caller and kept the
upstream open after the route cancelled a recording it refused, which
the audio tests caught. Every caller already passes its own signal.

no-unguarded-fetch also flags a fetch handed in as a dependency
(request.fetch, this.fetch) in a function that reads a host, as the
audio route did with request.pds(did); identity.fetch and
identity.serviceFetch are left alone. It flags main's audio.ts and
nothing on this tree.

The DNS rebinding helper moves to testing/rebind.ts, where
bnb/oauth-guarded-fetch put it, and a new route test streams a
recording from a PDS whose name points at loopback when it connects:
refused, with no connection. It reaches the loopback server on main's
route.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Tests, fixtures and test harnesses web: server Spaces, sync, HappyView, the database and the server around them web The Local Network website (apps/web)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants