Private feed keys in the URL path reach every signed-in user through /api/events #325

Closed
opened 2026-10-23 05:26:45 -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.

/api/events sends every daemon Event to every signed-in user, whether or not they subscribe to the feed. Before an event goes out, Event::redacted strips only the query, fragment and userinfo from URLs and from error text. A paid feed that carries the subscriber key in its path, such as feeds.supercast.com/feeds/, keeps the key. The codebase treats exactly that URL shape as private elsewhere: url_looks_private counts every PAID_HOSTS URL as private, and the directory snapshot leaves such feeds out. When such a feed fails, its FeedError msg (reqwest's 'error sending request for url (...)' or ipx's own 'error following redirect for url (...)') carries the full path to every open SSE stream. A permanent move does the same through FeedMoved from/to. A signed-in non-admin household user who does not subscribe can then read the key and use the paid feed and its episodes as the owner. redact's own doc comment records path tokens as a known shortcut (feed/src/lib.rs:87). That explains the gap but does not close it.

Claimed root cause. feed::redact and redact_text (crates/feed/src/lib.rs:88-115) treat only the query, fragment and userinfo as secret. A key in the path is a documented shortcut (line 87). The broadcast in web.rs events() relies on that redaction as its only control and does not filter by subscription. This disagrees with url_looks_private (feed/src/lib.rs:1274-1291), which treats any URL on a PAID_HOSTS host as private because of keys in the path.

Local check. {'local': 'Inside a sandbox, add a unit test to crates/ipx/src/ipc.rs. Build Event::FeedError { feed: "paid".into(), msg: "error sending request for url (https://feeds.supercast.com/feeds/SECRETKEY): operation timed out".into() }.redacted() and Event::FeedMoved { feed: "paid".into(), from: "https://feeds.supercast.com/feeds/SECRETKEY".into(), to: "https://x.supercast.com/feeds/SECRETKEY2".into() }.redacted(), then assert that the serialised JSON does not contain SECRETKEY. Expected today: it does. End to end, in a scratch daemon: dummy user A adds a fixture feed served at http://127.0.0.1:/feeds/SECRETKEY that loops redirects. Dummy user B (non-admin, not subscribed) reads GET /api/events and should receive a feed_error event whose msg contains /feeds/SECRETKEY.', 'deployment': 'The owner checks, read-only, whether any catalogue feed URL is on a PAID_HOSTS host or has a long opaque path segment. If none is, the impact is latent.'}

Owner check (no probing). The owner checks, read-only, whether any catalogue feed URL is on a PAID_HOSTS host or has a long opaque path segment. If none is, the impact is latent.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:ipc.rs:Event::redacted:path-key-in-sse-broadcast).

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. /api/events sends every daemon Event to every signed-in user, whether or not they subscribe to the feed. Before an event goes out, Event::redacted strips only the query, fragment and userinfo from URLs and from error text. A paid feed that carries the subscriber key in its path, such as feeds.supercast.com/feeds/<key>, keeps the key. The codebase treats exactly that URL shape as private elsewhere: url_looks_private counts every PAID_HOSTS URL as private, and the directory snapshot leaves such feeds out. When such a feed fails, its FeedError msg (reqwest's 'error sending request for url (...)' or ipx's own 'error following redirect for url (...)') carries the full path to every open SSE stream. A permanent move does the same through FeedMoved from/to. A signed-in non-admin household user who does not subscribe can then read the key and use the paid feed and its episodes as the owner. redact's own doc comment records path tokens as a known shortcut (feed/src/lib.rs:87). That explains the gap but does not close it. **Claimed root cause.** feed::redact and redact_text (crates/feed/src/lib.rs:88-115) treat only the query, fragment and userinfo as secret. A key in the path is a documented shortcut (line 87). The broadcast in web.rs events() relies on that redaction as its only control and does not filter by subscription. This disagrees with url_looks_private (feed/src/lib.rs:1274-1291), which treats any URL on a PAID_HOSTS host as private because of keys in the path. **Local check.** {'local': 'Inside a sandbox, add a unit test to crates/ipx/src/ipc.rs. Build Event::FeedError { feed: "paid".into(), msg: "error sending request for url (https://feeds.supercast.com/feeds/SECRETKEY): operation timed out".into() }.redacted() and Event::FeedMoved { feed: "paid".into(), from: "https://feeds.supercast.com/feeds/SECRETKEY".into(), to: "https://x.supercast.com/feeds/SECRETKEY2".into() }.redacted(), then assert that the serialised JSON does not contain SECRETKEY. Expected today: it does. End to end, in a scratch daemon: dummy user A adds a fixture feed served at http://127.0.0.1:<port>/feeds/SECRETKEY that loops redirects. Dummy user B (non-admin, not subscribed) reads GET /api/events and should receive a feed_error event whose msg contains /feeds/SECRETKEY.', 'deployment': 'The owner checks, read-only, whether any catalogue feed URL is on a PAID_HOSTS host or has a long opaque path segment. If none is, the impact is latent.'} **Owner check (no probing).** The owner checks, read-only, whether any catalogue feed URL is on a PAID_HOSTS host or has a long opaque path segment. If none is, the impact is latent. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:ipc.rs:Event::redacted:path-key-in-sse-broadcast`).
rays added the bug label 2026-10-23 05:26:45 -07:00
Author
Owner

Fixed in 91de111: /api/events sends an event about a feed only to its subscribers and to admins (Event::is_for); Reaped goes to admins only, SignalHits to its own person. Deployed; a forced check of a subscribed feed still streams feed_start, feed_done, feed_row and scan_done.

Fixed in 91de111: /api/events sends an event about a feed only to its subscribers and to admins (Event::is_for); Reaped goes to admins only, SignalHits to its own person. Deployed; a forced check of a subscribed feed still streams feed_start, feed_done, feed_row and scan_done.
rays closed this issue 2026-10-23 07:53:04 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#325