Only a subscriber or an admin marks a feed private (#333)

patch_feed let private = true through for anyone signed in, as the
field's doc comment meant for subscribers, but never checked that the
caller subscribed. Ids are slugs of titles, so anyone could mark
another person's feed, or a whole OPML (whose shows go with it),
private, keeping it out of the directory, and promote a derived feed
they did not read out of its OPML; only an admin could undo either.

A non-admin now needs a subscription to the feed, or gets 404, as the
per-person settings above already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 14:56:36 +00:00
parent 91de1114d8
commit daf95a25cd
3 changed files with 8 additions and 0 deletions

View File

@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ipx user passwd` signs the account out of every browser, and says how many of its API tokens still work. A session signed in with the old password went on working (#334).
- Live updates about a feed go only to the people who read it, and admins. Everyone signed in had every feed's, errors included, which carried a paid feed's address with its key (#325).
- An open page stops getting live updates within half a minute of its account being removed. They went on until the page was closed (#335).
- Only someone subscribed to a feed, or an admin, may mark it private. Anyone signed in could keep anyone's feed, or a whole OPML's, out of the directory, and only an admin could undo it (#333).
## [1.4.0] - 2026-10-19

View File

@@ -1822,6 +1822,11 @@ async fn patch_feed(
"the feed's address, folder, schedule and category, and whether it may be shared, are the same for everyone, so only an admin changes them",
));
}
// Marking a feed private is for those who read it: anyone could keep anyone's feed, or a
// whole OPML's, out of the directory, and only an admin could undo it (#333).
if !user.is_admin && state.ctx.db.subscription(user.id, &id).instrument(step("subscription")).await?.is_none() {
return Err(ApiError::not_found("you do not subscribe to that feed"));
}
let mut cfg = state.ctx.edit_cfg().await;
// Derived feeds have no config entry. Editing one is the moment it earns a real