PST-032: A captures-only token reads the whole pending review queue, including company, location and URL of every capture #761

Closed
opened 2026-10-10 10:56:07 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-032. Severity: Low.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Low
  • Category: scope-creep / information-disclosure
  • Location: src/postulo/api/api.py (650-680)
  • Found by: REST API

Who can do it, and what they need. Someone holding a leaked browser-extension token (scope captures only). Precondition: the token, which the threat model treats as the least trusted credential Postulo issues.

How it works.

  1. GET /api/v1/captures with the captures-only token.
  2. Every pending capture of the account is returned (up to 200 per page, paginated), whatever tool created it (web form, another extension, a plugin), with title, company_name, location, url, review_url and page details. Verified: a capture created with origin='web' and data {company_name:'Hush Corp', location:'Lisbon'} came back in full to a captures-only token.

Impact. The threat model states a leaked extension token can 'fill a review queue' and, when asking whether a posting is held, is told 'the title, the date and a link, never the company, the address or where the person stands with it'. The list endpoint hands over the company and location of every posting the person is currently considering, which is a picture of their job search. Limited scope (pending captures only), hence medium at most; may be judged intended since the operation is documented under the scope.

Evidence.

api.py 382-396: `return {"id": capture.pk, "url": capture.url, "title": data.get("title", ""), "company_name": data.get("company_name", ""), "location": data.get("location", ""), ... "review_url": ...}` returned by `list_captures` which is `auth=scope("captures")` (api.py 653).

Verification (verdict: confirmed; severity after verification: Low; first-pass claim: Medium). File and lines are right (api.py 647-677, CaptureOut 353-365, _as_output 392-406). Correct the framing: this is a documented and tested operation of the captures scope, so the fix is a policy decision. Either return only title/date/link for a captures-only token, as POST /captures/known does, or amend the threat-model row and the scope label to say the token can read the pending queue including company and location. It is not cross-account (for_user() is applied) and shows only pending captures. Precondition: a leaked extension token plus the instance URL.

Suggested remediation. Either narrow GET /captures for a captures-only token to the fields KnownCaptureOut already limits to (id, title, created_at, review_url), returning the full shape only to a token that also holds read; or move the list under read and update the threat model/wiki to say what a captures token can see.

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-032**. Severity: **Low**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Low - **Category:** scope-creep / information-disclosure - **Location:** `src/postulo/api/api.py` (650-680) - **Found by:** REST API **Who can do it, and what they need.** Someone holding a leaked browser-extension token (scope `captures` only). Precondition: the token, which the threat model treats as the least trusted credential Postulo issues. **How it works.** 1. `GET /api/v1/captures` with the captures-only token. 2. Every pending capture of the account is returned (up to 200 per page, paginated), whatever tool created it (web form, another extension, a plugin), with `title`, `company_name`, `location`, `url`, `review_url` and `page` details. Verified: a capture created with origin='web' and data {company_name:'Hush Corp', location:'Lisbon'} came back in full to a captures-only token. **Impact.** The threat model states a leaked extension token can 'fill a review queue' and, when asking whether a posting is held, is told 'the title, the date and a link, never the company, the address or where the person stands with it'. The list endpoint hands over the company and location of every posting the person is currently considering, which is a picture of their job search. Limited scope (pending captures only), hence medium at most; may be judged intended since the operation is documented under the scope. **Evidence.** ```text api.py 382-396: `return {"id": capture.pk, "url": capture.url, "title": data.get("title", ""), "company_name": data.get("company_name", ""), "location": data.get("location", ""), ... "review_url": ...}` returned by `list_captures` which is `auth=scope("captures")` (api.py 653). ``` **Verification** (verdict: confirmed; severity after verification: Low; first-pass claim: Medium). File and lines are right (api.py 647-677, CaptureOut 353-365, _as_output 392-406). Correct the framing: this is a documented and tested operation of the `captures` scope, so the fix is a policy decision. Either return only title/date/link for a captures-only token, as POST /captures/known does, or amend the threat-model row and the scope label to say the token can read the pending queue including company and location. It is not cross-account (for_user() is applied) and shows only pending captures. Precondition: a leaked extension token plus the instance URL. **Suggested remediation.** Either narrow `GET /captures` for a captures-only token to the fields `KnownCaptureOut` already limits to (id, title, created_at, review_url), returning the full shape only to a token that also holds `read`; or move the list under `read` and update the threat model/wiki to say what a captures token can see.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:39 +00:00
Author
Owner

Fixed in 3aa2b8f53 on main.

Fixed in 3aa2b8f53 on main.
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
Postulo/postulo#761
No description provided.