A kept feed from a private OPML loses its private mark when the OPML is removed and is shared to the directory #332

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

A user subscribes to an OPML or Patreon list and marks it private (#205; patch_feed lets any subscriber set private=true, only private=false is admin-only), which keeps every feed derived from it out of the directory snapshot through the group check in snapshot_entries. The user marks one derived feed as kept (#280). When the list loses its last subscriber (leave, also reached from remove_user), or is removed by rm, clean_directory or retire_stranded, the caller removes the parent from the config and stores it before calling retire_group. retire_group then calls promote_kept, which looks the parent up in the already-updated config to copy its settings: it is gone, so the new standalone catalogue entry gets private=false, no group and no username/password/password_env. snapshot_entries no longer sees a private group or a private flag, and store_cfg wakes the directory task, so the feed's URL is sent in the next /v1/snapshot to the directory (shared with other iPX servers and its public pages) unless url_looks_private's heuristic (paid hosts, URL userinfo, credential-like query keys) happens to match it. The sync_group path, where the parent is still in config, keeps the flag, showing the intended behaviour. Source confirms every step; impact is disclosure of a household member's feed URL that they intended to keep private, bounded to feeds that evade the URL heuristic.

Claimed root cause. promote_kept derives the promoted feed's privacy and credentials from cfg.feeds.get(&m.group_id), but every retire_group caller (leave, rm, clean_directory, retire_stranded) has already removed the parent from the stored config, so the parent's private flag and credentials are silently dropped at promotion.

Local check. {'local': 'In a sandbox, extend a_kept_feed_outlives_its_opml_for_whoever_kept_it in crates/ipx/src/main.rs: insert the parent 'opml' with private=true, a derived feed 'a' at http://x/feeds/0123456789abcdef.xml via upsert_managed, one dummy user subscribed to both with set_kept on 'a'; call leave(&ctx, me, "opml"); assert ctx.cfg().feeds["a"].private is true and that crate::directory::snapshot_entries(&subscriptions(&ctx).await?, &counts) has no entry with that URL. Expected on current code: private is false and the URL is in the snapshot. Run cargo test -p ipx a_kept_feed.', 'deployment': 'Owner checks, read only, whether any subscribed OPML/Patreon list is marked private and has kept children, whether any catalogue feed with no group and private=false was promoted from such a list, and whether a directory token is set in Admin, Directory.'}

Owner check (no probing). Owner checks, read only, whether any subscribed OPML/Patreon list is marked private and has kept children, whether any catalogue feed with no group and private=false was promoted from such a list, and whether a directory token is set in Admin, Directory.

Full detail: /config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md (fingerprint ipx:main.rs:promote_kept:private-flag-lost-when-parent-already-removed).

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. A user subscribes to an OPML or Patreon list and marks it private (#205; patch_feed lets any subscriber set private=true, only private=false is admin-only), which keeps every feed derived from it out of the directory snapshot through the group check in snapshot_entries. The user marks one derived feed as kept (#280). When the list loses its last subscriber (leave, also reached from remove_user), or is removed by rm, clean_directory or retire_stranded, the caller removes the parent from the config and stores it before calling retire_group. retire_group then calls promote_kept, which looks the parent up in the already-updated config to copy its settings: it is gone, so the new standalone catalogue entry gets private=false, no group and no username/password/password_env. snapshot_entries no longer sees a private group or a private flag, and store_cfg wakes the directory task, so the feed's URL is sent in the next /v1/snapshot to the directory (shared with other iPX servers and its public pages) unless url_looks_private's heuristic (paid hosts, URL userinfo, credential-like query keys) happens to match it. The sync_group path, where the parent is still in config, keeps the flag, showing the intended behaviour. Source confirms every step; impact is disclosure of a household member's feed URL that they intended to keep private, bounded to feeds that evade the URL heuristic. **Claimed root cause.** promote_kept derives the promoted feed's privacy and credentials from cfg.feeds.get(&m.group_id), but every retire_group caller (leave, rm, clean_directory, retire_stranded) has already removed the parent from the stored config, so the parent's private flag and credentials are silently dropped at promotion. **Local check.** {'local': 'In a sandbox, extend a_kept_feed_outlives_its_opml_for_whoever_kept_it in crates/ipx/src/main.rs: insert the parent \'opml\' with private=true, a derived feed \'a\' at http://x/feeds/0123456789abcdef.xml via upsert_managed, one dummy user subscribed to both with set_kept on \'a\'; call leave(&ctx, me, "opml"); assert ctx.cfg().feeds["a"].private is true and that crate::directory::snapshot_entries(&subscriptions(&ctx).await?, &counts) has no entry with that URL. Expected on current code: private is false and the URL is in the snapshot. Run `cargo test -p ipx a_kept_feed`.', 'deployment': 'Owner checks, read only, whether any subscribed OPML/Patreon list is marked private and has kept children, whether any catalogue feed with no group and private=false was promoted from such a list, and whether a directory token is set in Admin, Directory.'} **Owner check (no probing).** Owner checks, read only, whether any subscribed OPML/Patreon list is marked private and has kept children, whether any catalogue feed with no group and private=false was promoted from such a list, and whether a directory token is set in Admin, Directory. Full detail: `/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md` (fingerprint `ipx:main.rs:promote_kept:private-flag-lost-when-parent-already-removed`).
rays added the bug label 2026-10-23 05:26:49 -07:00
Author
Owner

Fixed in 4f7dab9: retire_group and promote_kept take the list's settings from the caller, captured before the list is removed from config, so a kept show keeps private and the credentials. When the list is no longer known (retire_stranded), the promoted show is private. The existing kept-feed test now uses a private OPML and checks both paths. Deployed.

Fixed in 4f7dab9: retire_group and promote_kept take the list's settings from the caller, captured before the list is removed from config, so a kept show keeps private and the credentials. When the list is no longer known (retire_stranded), the promoted show is private. The existing kept-feed test now uses a private OPML and checks both paths. Deployed.
rays closed this issue 2026-10-23 07:00:10 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#332