diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-11-06 09:18:57 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-11-06 09:18:57 +0000 |
| commit | 68a8d692f03ddc48aaf70399bfb1cc7584e21810 (patch) | |
| tree | fbaa1d0ee1271eeea25f7f6c0e67b5dd7c1ed2f2 /server/cmd | |
| parent | 816118d8860093f3c9726b45c41c34c4a2751a2a (diff) | |
A feed must not be able to spend the server's token
The security review of the last commit found a confused deputy in it, and it was right. The
bearer was attached on origin alone, and `episode.mediaUrl` comes out of a feed -- so a publisher
could set an enclosure URL on the skip server's own origin, match the origin check exactly, and
have the app issue an authenticated GET to a path of their choosing, storing whatever came back
as that episode's audio. Confirmed by writing the test before the fix: it failed.
Provenance is the first gate now and origin is the second. `authenticated = true` is passed only
for the URL `ServerAudio.resolve` returned, never for one that arrived in a feed, and the origin
check still has to agree -- so a server answer naming some other host is fetched without
credentials rather than sending them there. Both gates have a test, including the case where they
disagree, and OutboundRequestsTest pins that exactly one call site in the app is authenticated.
**Chasing the coverage gate honestly turned up a real bug**, which is the second time this week
it has earned its keep. Rather than file a third COVERAGE_EXCEPTION I went to see what the
uncovered lines in EpisodeDownloader actually were, and wrote a test for the empty-response
branch. It failed: OkHttp hands back an empty body rather than null for a 204, so the existing
`?: run { ... }` guard never fires, the copy writes nothing, and a **zero-byte file is renamed
into place and recorded as DOWNLOADED**. That episode then fails to play and reads as the
publisher's fault. The length is checked after the copy now, which catches a 204, an empty 200
and anything else that arrives with no bytes.
Also tested while there: the media-type to file-extension mapping, which had none. Not cosmetic
-- Media3 picks an extractor partly from the extension, so a .audio file holding mp4 bytes is a
plausible way to get an episode that will not play. Parameters and capitals included, because
feeds send "audio/mpeg; charset=utf-8" and "AUDIO/MPEG".
1059 tests, detekt 0, lint clean, coverage 94.09% -> 94.28% with no exception.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'server/cmd')
0 files changed, 0 insertions, 0 deletions
