Any signed-in user can mark a feed they do not subscribe to private, and promote an OPML-derived feed out of its OPML #333
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.
PATCH /api/feeds/{id} with body {"private": true} from a signed-in non-admin user passes patch_feed's admin gate, because private == Some(true) is not counted as admin-only, and nothing on the path checks that the caller subscribes to the feed. The handler then edits the shared catalogue: if the id names an OPML-derived feed that is not in cfg.feeds, it is found in crate::subscriptions (the whole server's catalogue plus managed derived feeds), inserted into the catalogue and db.unmanage'd; then feed.private = true is stored with ctx.store_cfg. Feed ids are slugs of titles, so they can be guessed. Effect, bounded to what source shows: another household user's feed, or a whole OPML group (whose children snapshot_entries also withholds), is dropped from the directory snapshot, and only an admin can undo it (private=false is admin-only); a derived feed the caller does not read is promoted to a catalogue entry and stops being managed by its OPML. No other user's data is disclosed. The field's own doc comment limits this action to subscribers.
Claimed root cause. patch_feed treats private=Some(true) as a feed-wide change open to non-admins (admin_only excludes it) but never calls subscribed() or db.subscription() before edit_cfg, the derived-feed promotion and store_cfg, unlike the per-user fields earlier in the handler, which are applied only inside
if let Some(mine) = state.ctx.db.subscription(user.id, &id)or refused when set_pinned/set_kept/set_hide_* return false.Local check. {'local': 'In a scratch daemon (Db::memory unit test or the Playwright fixture daemon under /tmp/ipx-ui-test, never production), create admin A and non-admin B. As A, add fixture feed F (not private) and an OPML O with child C. As B, subscribed to neither, send PATCH /api/feeds/F {"private": true} and PATCH /api/feeds/C {"private": true}. Expected today: 204 for both; GET /api/feeds as A shows F private:true; C is now in the stored catalogue and feeds.managed is false for C; directory::snapshot_entries omits F and C. Then as B send PATCH /api/feeds/F {"private": false} and expect 403, showing only an admin can undo it. Expected after the fix: 404 'you do not subscribe to that feed' and no change. Fix: before
let mut cfg = state.ctx.edit_cfg()requireuser.is_admin || state.ctx.db.subscription(user.id, &id).await?.is_some()(or call subscribed()), and add a unit test for the non-subscriber case.', 'deployment': "Owner-observed, non-destructive: in production's Admin, check whether any feed is marked private that none of its subscribers marked; no request needs to be sent."}Owner check (no probing). Owner-observed, non-destructive: in production's Admin, check whether any feed is marked private that none of its subscribers marked; no request needs to be sent.
Full detail:
/config/security-audit-skill/ipx/run-1/NEEDS-VALIDATION.md(fingerprintipx-patch_feed-private-without-subscription).Fixed in
daf95a2: patch_feed refuses private=true with 404 from a non-admin who does not subscribe to the feed, before the catalogue is touched. A browser test checks it. Deployed.