Skip to content

fetch_feed and download_episode silently treat HTTP 4xx/5xx bodies as valid content #455

Description

@forkwright

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.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions