domain: Micropub q=source has no scope check, create-only token can read private/draft posts #50

Closed
opened 2026-07-03 01:41:43 +00:00 by rosa · 1 comment
Owner

Found during a code review of the domain crate.

Location: crates/domain/src/services.rs:604 (micropub_source)
Severity: Security — private content disclosure

Problem

micropub_create/update/delete each enforce auth.scopes.contains(...), but micropub_source (q=source) performs no scope check, and the web q=source handler gates on token presence alone. Any valid token, including a create-only token, can read the full editable source of the owner private and draft posts.

Failure scenario

A user grants a client only create; a leaked copy of that token calls q=source and retrieves the raw content of the owner private and draft posts, which the narrowed consent was meant to prevent.

Suggested fix

Require an explicit read scope for q=source (decide the policy deliberately rather than by omission), ideally via a single shared required-scope gate every Micropub entry point pays.

Found during a code review of the `domain` crate. **Location:** `crates/domain/src/services.rs:604` (`micropub_source`) **Severity:** Security — private content disclosure ## Problem `micropub_create`/`update`/`delete` each enforce `auth.scopes.contains(...)`, but `micropub_source` (q=source) performs no scope check, and the web q=source handler gates on token presence alone. Any valid token, including a create-only token, can read the full editable source of the owner private and draft posts. ## Failure scenario A user grants a client only `create`; a leaked copy of that token calls q=source and retrieves the raw content of the owner private and draft posts, which the narrowed consent was meant to prevent. ## Suggested fix Require an explicit read scope for q=source (decide the policy deliberately rather than by omission), ideally via a single shared required-scope gate every Micropub entry point pays.
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: bug (security — private content disclosure)
Summary: Micropub q=source must require the update scope; today any valid token can read the full editable source of the owner's private and draft posts.

Policy decided during triage: q=source is gated on the existing update scope. No new read scope is introduced — q=source exists so editing clients can fetch a post's source before updating it, and update is the consent that implies that need. A create-only token must not be able to read private/draft source.

Current behavior:
The domain-service entry points for Micropub create, update, and delete each enforce their scope (create, update, delete) before doing anything else. The q=source entry point does not: it resolves ownership of the post behind the given url and returns the post, and the web handler for q=source gates on token presence alone. Any authenticated token — including one granted only create — can retrieve the raw editable source of the owner's private and draft posts.

Desired behavior:
A q=source request made with a token that lacks the update scope is rejected with the same insufficient-scope error the other Micropub entry points produce (same error variant, same HTTP status and error body shape on the wire). The scope check happens before post resolution, consistent with the other entry points, so a scope-less caller learns nothing about whether the URL resolves to a post. A token holding update continues to receive the source of posts the token's user owns, regardless of the post's visibility or draft status.

Key interfaces:

  • The Micropub source method on the app service (currently micropub_source) — must enforce Scope::Update before resolving the post, returning the same insufficient-scope MicropubError variant the other entry points use.
  • The issue suggests a single shared required-scope gate that every Micropub entry point pays (rather than four hand-rolled contains checks). Adopting that refactor is welcome but optional; if done, it must not change the scope each existing endpoint requires or the error shape it returns.

Acceptance criteria:

  • q=source with a token lacking update (e.g. create-only) returns the insufficient-scope error, with the same status and body shape as the other Micropub endpoints' scope failures.
  • The rejection does not reveal whether the requested URL corresponds to an existing post.
  • q=source with a token holding update still returns the editable source for the token owner's posts, including drafts and private posts.
  • Scope enforcement for create, update, and delete is unchanged.
  • A test covers the create-only-token-rejected-on-q=source scenario.

Out of scope:

  • Adding a read (or any new) scope to the scope vocabulary, the IndieAuth consent flow, or token parsing.
  • Changing ownership/visibility rules for q=source beyond the scope gate.
  • Other Micropub queries (e.g. q=config) or the web (non-Micropub) surface.
> *This was generated by AI during triage.* ## Agent Brief **Category:** bug (security — private content disclosure) **Summary:** Micropub `q=source` must require the `update` scope; today any valid token can read the full editable source of the owner's private and draft posts. **Policy decided during triage:** `q=source` is gated on the existing `update` scope. No new `read` scope is introduced — `q=source` exists so editing clients can fetch a post's source before updating it, and `update` is the consent that implies that need. A create-only token must not be able to read private/draft source. **Current behavior:** The domain-service entry points for Micropub create, update, and delete each enforce their scope (`create`, `update`, `delete`) before doing anything else. The `q=source` entry point does not: it resolves ownership of the post behind the given `url` and returns the post, and the web handler for `q=source` gates on token presence alone. Any authenticated token — including one granted only `create` — can retrieve the raw editable source of the owner's private and draft posts. **Desired behavior:** A `q=source` request made with a token that lacks the `update` scope is rejected with the same insufficient-scope error the other Micropub entry points produce (same error variant, same HTTP status and error body shape on the wire). The scope check happens before post resolution, consistent with the other entry points, so a scope-less caller learns nothing about whether the URL resolves to a post. A token holding `update` continues to receive the source of posts the token's user owns, regardless of the post's visibility or draft status. **Key interfaces:** - The Micropub source method on the app service (currently `micropub_source`) — must enforce `Scope::Update` before resolving the post, returning the same insufficient-scope `MicropubError` variant the other entry points use. - The issue suggests a single shared required-scope gate that every Micropub entry point pays (rather than four hand-rolled `contains` checks). Adopting that refactor is welcome but optional; if done, it must not change the scope each existing endpoint requires or the error shape it returns. **Acceptance criteria:** - [ ] `q=source` with a token lacking `update` (e.g. create-only) returns the insufficient-scope error, with the same status and body shape as the other Micropub endpoints' scope failures. - [ ] The rejection does not reveal whether the requested URL corresponds to an existing post. - [ ] `q=source` with a token holding `update` still returns the editable source for the token owner's posts, including drafts and private posts. - [ ] Scope enforcement for create, update, and delete is unchanged. - [ ] A test covers the create-only-token-rejected-on-`q=source` scenario. **Out of scope:** - Adding a `read` (or any new) scope to the scope vocabulary, the IndieAuth consent flow, or token parsing. - Changing ownership/visibility rules for `q=source` beyond the scope gate. - Other Micropub queries (e.g. `q=config`) or the web (non-Micropub) surface.
rosa closed this issue 2026-07-03 05:25:15 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rosa/vernier#50
No description provided.