Feeds listed in a password-protected OPML inherit its username and password and send them to their own, unrelated hosts #327

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.

When the operator sets username and password (or password_env) on a catalogue feed whose body turns out to be OPML, such as a private feed list on their own server, every feed the OPML lists gets that credential. subscriptions() builds each derived feed's config::Feed with the parent's username, password and password_env. The scan then calls feed::fetch(child_url, auth()), and fetch's same-host guard compares against the child's own URL, so the check always passes and the parent's password goes in an Authorization header to whatever host the OPML author named in an xmlUrl, on every scan. download::download also sends basic auth to the child's enclosure URLs on any host, with no host check at all. promote_kept() copies the same credential into a separate catalogue entry for a kept child, so that child keeps it after the OPML is removed. Only the operator can set a credential (add_feed in web.rs sets username and password to None), so this needs an operator-configured credentialed OPML or Patreon list.

Claimed root cause. subscriptions() (main.rs:1399-1401) and promote_kept() (main.rs:1507-1509) copy username, password and password_env from the parent list to each derived feed without comparing hosts. feed::fetch's same-host guard (feed/src/lib.rs:153-157) therefore checks against the child's URL instead of the host the credential was issued for.

Local check. {'local': 'Unit-test subscriptions(): build a Ctx whose catalogue has a feed {url: 'https://lists.example/private.opml', username: 'u', password: 'p'} and a managed_feeds row {id: 'child', url: 'https://other.example/rss', group_id: }. Assert that the returned Sub for 'child' has username None; at this commit it is Some("u"). End to end, in a sandbox: serve an OPML on localhost:A that requires basic auth and lists http://127.0.0.1:B/rss, run one scan, and check whether the listener on 127.0.0.1:B receives 'Authorization: Basic dTpw'. host_str ignores the port, so the two URLs need distinct hostnames.', 'deployment': "Owner checks the catalogue table (read only) for any feed with username or password_env whose URL serves an OPML or is a Patreon creator, and whether any of that feed's managed children are on another host."}

Owner check (no probing). Owner checks the catalogue table (read only) for any feed with username or password_env whose URL serves an OPML or is a Patreon creator, and whether any of that feed's managed children are on another host.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:main:derived-feed-inherits-parent-basic-auth).

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. When the operator sets username and password (or password_env) on a catalogue feed whose body turns out to be OPML, such as a private feed list on their own server, every feed the OPML lists gets that credential. subscriptions() builds each derived feed's config::Feed with the parent's username, password and password_env. The scan then calls feed::fetch(child_url, auth()), and fetch's same-host guard compares against the child's own URL, so the check always passes and the parent's password goes in an Authorization header to whatever host the OPML author named in an xmlUrl, on every scan. download::download also sends basic auth to the child's enclosure URLs on any host, with no host check at all. promote_kept() copies the same credential into a separate catalogue entry for a kept child, so that child keeps it after the OPML is removed. Only the operator can set a credential (add_feed in web.rs sets username and password to None), so this needs an operator-configured credentialed OPML or Patreon list. **Claimed root cause.** subscriptions() (main.rs:1399-1401) and promote_kept() (main.rs:1507-1509) copy username, password and password_env from the parent list to each derived feed without comparing hosts. feed::fetch's same-host guard (feed/src/lib.rs:153-157) therefore checks against the child's URL instead of the host the credential was issued for. **Local check.** {'local': 'Unit-test subscriptions(): build a Ctx whose catalogue has a feed {url: \'https://lists.example/private.opml\', username: \'u\', password: \'p\'} and a managed_feeds row {id: \'child\', url: \'https://other.example/rss\', group_id: <parent id>}. Assert that the returned Sub for \'child\' has username None; at this commit it is Some("u"). End to end, in a sandbox: serve an OPML on localhost:A that requires basic auth and lists http://127.0.0.1:B/rss, run one scan, and check whether the listener on 127.0.0.1:B receives \'Authorization: Basic dTpw\'. host_str ignores the port, so the two URLs need distinct hostnames.', 'deployment': "Owner checks the catalogue table (read only) for any feed with username or password_env whose URL serves an OPML or is a Patreon creator, and whether any of that feed's managed children are on another host."} **Owner check (no probing).** Owner checks the catalogue table (read only) for any feed with username or password_env whose URL serves an OPML or is a Patreon creator, and whether any of that feed's managed children are on another host. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:main:derived-feed-inherits-parent-basic-auth`).
rays added the bug label 2026-10-23 05:26:47 -07:00
Author
Owner

Fixed in 80f12cd: a feed derived from an OPML or Patreon list takes the list's credentials only when it is on the list's own host (config::Feed::credentials_for), in subscriptions() and promote_kept() both. Deployed.

Fixed in 80f12cd: a feed derived from an OPML or Patreon list takes the list's credentials only when it is on the list's own host (config::Feed::credentials_for), in subscriptions() and promote_kept() both. 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#327