Finding
Neither fetch_feed nor download_episode validates the HTTP status before consuming the response body. A server returning 401, 403, 404, 429, or 500 hands an error-HTML or JSON body directly to parse_feed (or writes it to disk as audio content). For fetch_feed the body either fails to parse (propagating a FeedParse error) or — if it happens to parse as a valid-but-empty feed — silently clears all episodes via the dedup/retention path on the next refresh. For download_episode any non-2xx body is written byte-for-byte to the destination file with no status check at all.
Evidence
crates/komide/src/fetch.rs:39
if response.status() == StatusCode::NOT_MODIFIED {
This is the only status check in fetch_feed; for any status other than 304 the body at lines 56-61 is consumed unconditionally. download_episode (lines 78-98) performs no status check and persists the body regardless of status code.
Why this matters
Under an active counter-surveillance threat model, feed and media endpoints are exactly where an adversary or compromised CDN can inject content. A rate-limited (429) or auth-expired (401) feed URL causes download_episode to persist an attacker-controlled HTML error page at the episode file path; because the file then exists with non-zero size, downstream callers skip re-download and the corruption is silent and persistent — a poisoned payload masquerading as expected audio. For fetch_feed, a 503 carrying a valid XML body (some CDN error pages) can wipe the episode list on the next refresh, a denial-of-record that destroys local state the user may be relying on.
Desired correction
Gate body consumption on success in both functions by calling .error_for_status() immediately after send().await, e.g.:
let response = req.send().await.context(FeedFetchSnafu { url: url.to_string() })?
.error_for_status().map_err(|e| FeedFetchSnafu { url: url.to_string() }.into_error(e))?;
The 304 Not Modified path must remain handled before this check (it is not an error). Done when: a mock server returning HTTP 500 causes both fetch_feed and download_episode to return Err(KomideError::FeedFetch{..}) rather than consuming the body or writing it to disk.
Finding
Neither
fetch_feednordownload_episodevalidates the HTTP status before consuming the response body. A server returning 401, 403, 404, 429, or 500 hands an error-HTML or JSON body directly toparse_feed(or writes it to disk as audio content). Forfetch_feedthe body either fails to parse (propagating aFeedParseerror) or — if it happens to parse as a valid-but-empty feed — silently clears all episodes via the dedup/retention path on the next refresh. Fordownload_episodeany non-2xx body is written byte-for-byte to the destination file with no status check at all.Evidence
crates/komide/src/fetch.rs:39This is the only status check in
fetch_feed; for any status other than304the body at lines 56-61 is consumed unconditionally.download_episode(lines 78-98) performs no status check and persists the body regardless of status code.Why this matters
Under an active counter-surveillance threat model, feed and media endpoints are exactly where an adversary or compromised CDN can inject content. A rate-limited (429) or auth-expired (401) feed URL causes
download_episodeto persist an attacker-controlled HTML error page at the episode file path; because the file then exists with non-zero size, downstream callers skip re-download and the corruption is silent and persistent — a poisoned payload masquerading as expected audio. Forfetch_feed, a 503 carrying a valid XML body (some CDN error pages) can wipe the episode list on the next refresh, a denial-of-record that destroys local state the user may be relying on.Desired correction
Gate body consumption on success in both functions by calling
.error_for_status()immediately aftersend().await, e.g.:The
304 Not Modifiedpath must remain handled before this check (it is not an error). Done when: a mock server returning HTTP 500 causes bothfetch_feedanddownload_episodeto returnErr(KomideError::FeedFetch{..})rather than consuming the body or writing it to disk.