summaryrefslogtreecommitdiff
path: root/gradle/wrapper
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-11-06 09:18:57 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-11-06 09:18:57 +0000
commit68a8d692f03ddc48aaf70399bfb1cc7584e21810 (patch)
treefbaa1d0ee1271eeea25f7f6c0e67b5dd7c1ed2f2 /gradle/wrapper
parent816118d8860093f3c9726b45c41c34c4a2751a2a (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 'gradle/wrapper')
0 files changed, 0 insertions, 0 deletions