Enclosure downloads send the feed's basic-auth password to whatever host the feed names, unlike feed fetches #328

Closed
opened 2026-10-23 05:26:47 -07:00 by rays · 1 comment
Owner

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it.

download::download (crates/ipx/src/download.rs:135-139) attaches the feed's configured username and password to the first request for every enclosure URL whenever feed_cfg.username is set, with no comparison of the enclosure's host to the feed's host. Enclosure URLs come from the feed XML (publisher-controlled), and podcast enclosures commonly go through third-party measurement or ad-insertion prefixes and CDNs, so those hosts receive the paid feed's credential in an Authorization header. feed::fetch (crates/feed/src/lib.rs:153-157) deliberately limits the same credential to the feed's own host, and Feed::auth() is documented as what is sent 'the feed's host', so the download path is the weaker sibling. Exposure is bounded: credentials are set only by the operator in the catalogue (web add_feed sets none), reqwest's default redirect policy on ctx.client (main.rs:358-365, no custom policy) strips Authorization on a cross-origin redirect so only the first, publisher-named host receives it, and that host is chosen by the same publisher who issued the credential or by a third party the publisher placed in the URL. Derived OPML children that inherit a parent's credential (separate candidate ipx:main:derived-feed-inherits-parent-basic-auth) reach this path too.

Claimed root cause. download.rs:136-137 calls req.basic_auth(user, feed_cfg.password()) unconditionally when feed_cfg.username is present, without the host binding feed::fetch applies at crates/feed/src/lib.rs:154-157; download() is not even given the feed URL to compare against.

Local check. {'local': 'In a sandbox, add a test in crates/ipx/src/download.rs: start a loopback listener bound to 'localhost' that records request headers and returns a small non-HTML body; call download() with a FeedCfg whose url is 'http://127.0.0.1:

/rss', username Some("u"), password Some("p") and enclosure URL 'http://localhost:/ep.mp3' with a scratch download_dir; assert no Authorization header is received (currently 'Basic dTpw' is expected to arrive). Fix: pass the feed URL into download() and attach basic_auth only when the enclosure URL's host equals the feed URL's host, as crates/feed/src/lib.rs:154-157 does.', 'deployment': "Owner lists catalogue feeds with a username set and checks, read only, whether their enclosure URLs (enclosures table) name any host other than the feed URL's host; any such host has been receiving the feed's password."}

Owner check (no probing). Owner lists catalogue feeds with a username set and checks, read only, whether their enclosure URLs (enclosures table) name any host other than the feed URL's host; any such host has been receiving the feed's password.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:download:enclosure-basic-auth-any-host).

Found by the security-audit skill (run-1, 2026-10-20). Source-read only: nothing was run, so this is a lead that still needs its local check, not a confirmed finding. Two independent verifiers did not disprove it. download::download (crates/ipx/src/download.rs:135-139) attaches the feed's configured username and password to the first request for every enclosure URL whenever feed_cfg.username is set, with no comparison of the enclosure's host to the feed's host. Enclosure URLs come from the feed XML (publisher-controlled), and podcast enclosures commonly go through third-party measurement or ad-insertion prefixes and CDNs, so those hosts receive the paid feed's credential in an Authorization header. feed::fetch (crates/feed/src/lib.rs:153-157) deliberately limits the same credential to the feed's own host, and Feed::auth() is documented as what is sent 'the feed's host', so the download path is the weaker sibling. Exposure is bounded: credentials are set only by the operator in the catalogue (web add_feed sets none), reqwest's default redirect policy on ctx.client (main.rs:358-365, no custom policy) strips Authorization on a cross-origin redirect so only the first, publisher-named host receives it, and that host is chosen by the same publisher who issued the credential or by a third party the publisher placed in the URL. Derived OPML children that inherit a parent's credential (separate candidate ipx:main:derived-feed-inherits-parent-basic-auth) reach this path too. **Claimed root cause.** download.rs:136-137 calls req.basic_auth(user, feed_cfg.password()) unconditionally when feed_cfg.username is present, without the host binding feed::fetch applies at crates/feed/src/lib.rs:154-157; download() is not even given the feed URL to compare against. **Local check.** {'local': 'In a sandbox, add a test in crates/ipx/src/download.rs: start a loopback listener bound to \'localhost\' that records request headers and returns a small non-HTML body; call download() with a FeedCfg whose url is \'http://127.0.0.1:<p>/rss\', username Some("u"), password Some("p") and enclosure URL \'http://localhost:<q>/ep.mp3\' with a scratch download_dir; assert no Authorization header is received (currently \'Basic dTpw\' is expected to arrive). Fix: pass the feed URL into download() and attach basic_auth only when the enclosure URL\'s host equals the feed URL\'s host, as crates/feed/src/lib.rs:154-157 does.', 'deployment': "Owner lists catalogue feeds with a username set and checks, read only, whether their enclosure URLs (enclosures table) name any host other than the feed URL's host; any such host has been receiving the feed's password."} **Owner check (no probing).** Owner lists catalogue feeds with a username set and checks, read only, whether their enclosure URLs (enclosures table) name any host other than the feed URL's host; any such host has been receiving the feed's password. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:download:enclosure-basic-auth-any-host`).
rays added the bug label 2026-10-23 05:26:47 -07:00
Author
Owner

Fixed in 80f12cd: download::download sends basic auth only when the enclosure is on the feed's own host (config::Feed::auth_for). Deployed.

Fixed in 80f12cd: download::download sends basic auth only when the enclosure is on the feed's own host (config::Feed::auth_for). Deployed.
rays closed this issue 2026-10-23 07:03:36 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#328