Capture API: a captures token can ask which postings the person holds and their state, which the threat model denies #536

Closed
opened 2026-10-01 10:47:28 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading every route the captures scope opens against what docs/THREAT-MODEL.md says it opens.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at api/api.py:761 (known_captures; its listing() helper at :772-780, state at :778) and api/api.py:287 (KnownIn.postings); jobs/known.py, api/models.py:30 and docs/THREAT-MODEL.md:20 have not moved.

The threat model says an extension's token "holds captures and nothing else, so the worst it can do is fill a review queue the person will decline", and that "it can send a page and cannot read one back". But POST /captures/known, under the same scope, answers questions about the person's listings: for up to 100 postings per request it returns every matching listing's title, company name, address, state (applied, discarded and so on) and creation time. The extension reads only the title, the creation time and the link, so the state, the company name and the address are handed out for nothing, and the documents describe a narrower scope than the code grants.

  • Where: api/api.py:759 known_captures (its listing() helper at :770-778 returns state at :776); api/api.py:285 KnownIn.postings (max_length=100); jobs/known.py:57-88 known (matches by address, and by exact title plus company); docs/THREAT-MODEL.md:20; api/models.py:30 ("captures": _("Capture postings")); the wiki's The capture API page, the scope table at line 21 and the paragraph at lines 27-28. The extension's use is sayKnown in postulo-chromium src/popup.js:208-236.
  • What happens: whoever holds an extension's token sends posting addresses, or title and company pairs, 100 at a time. For each one the person holds, the answer says so and gives its state and when it was created. GET /captures also lists the captures awaiting review, which the wiki does mention.
  • Who can trigger it: anyone holding a captures token, such as a copied browser profile or a compromised update of the extension. They can only confirm postings they can name, not list them all.
  • Why it matters: the scope reveals where the person has applied and what they decided about it, which is part of the job search the threat model says this token cannot reach. A person who chooses scopes by what the documents say is misinformed. The mail client's listings:bind scope is described precisely ("a title, a company, a place and a state"); this one is not.
  • Fix: give this route an output schema with only what the extension reads (id, title, created_at and listing_url), without state, company_name and url. Then correct docs/THREAT-MODEL.md:20, the captures label in SCOPES and the wiki's scope table and paragraph so they say that the scope also answers whether a posting the caller names is already held, with its title and date.

Done when

  • A test in tests/test_known.py asserts that the answer for a held listing carries no state, company_name or url key, and the threat model and the scope's label name the lookup.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, reading every route the `captures` scope opens against what `docs/THREAT-MODEL.md` says it opens. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `api/api.py:761` (`known_captures`; its `listing()` helper at `:772-780`, `state` at `:778`) and `api/api.py:287` (`KnownIn.postings`); `jobs/known.py`, `api/models.py:30` and `docs/THREAT-MODEL.md:20` have not moved. The threat model says an extension's token "holds `captures` and nothing else, so the worst it can do is fill a review queue the person will decline", and that "it can send a page and cannot read one back". But `POST /captures/known`, under the same scope, answers questions about the person's listings: for up to 100 postings per request it returns every matching listing's title, company name, address, state (`applied`, `discarded` and so on) and creation time. The extension reads only the title, the creation time and the link, so the state, the company name and the address are handed out for nothing, and the documents describe a narrower scope than the code grants. - **Where:** `api/api.py:759` `known_captures` (its `listing()` helper at `:770-778` returns `state` at `:776`); `api/api.py:285` `KnownIn.postings` (`max_length=100`); `jobs/known.py:57-88` `known` (matches by address, and by exact title plus company); `docs/THREAT-MODEL.md:20`; `api/models.py:30` (`"captures": _("Capture postings")`); the wiki's *The capture API* page, the scope table at line 21 and the paragraph at lines 27-28. The extension's use is `sayKnown` in postulo-chromium `src/popup.js:208-236`. - **What happens:** whoever holds an extension's token sends posting addresses, or title and company pairs, 100 at a time. For each one the person holds, the answer says so and gives its state and when it was created. `GET /captures` also lists the captures awaiting review, which the wiki does mention. - **Who can trigger it:** anyone holding a `captures` token, such as a copied browser profile or a compromised update of the extension. They can only confirm postings they can name, not list them all. - **Why it matters:** the scope reveals where the person has applied and what they decided about it, which is part of the job search the threat model says this token cannot reach. A person who chooses scopes by what the documents say is misinformed. The mail client's `listings:bind` scope is described precisely ("a title, a company, a place and a state"); this one is not. - **Fix:** give this route an output schema with only what the extension reads (`id`, `title`, `created_at` and `listing_url`), without `state`, `company_name` and `url`. Then correct `docs/THREAT-MODEL.md:20`, the `captures` label in `SCOPES` and the wiki's scope table and paragraph so they say that the scope also answers whether a posting the caller names is already held, with its title and date. ## Done when - A test in `tests/test_known.py` asserts that the answer for a held listing carries no `state`, `company_name` or `url` key, and the threat model and the scope's label name the lookup.
tiagoagueda added this to the 0.5.0 milestone 2026-10-01 10:47:28 +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
Postulo/postulo#536
No description provided.