SEC2-085: The extension asks for a captures token, while the threat model's mail-client row promises listings:bind and nothing else #14

Closed
opened 2026-10-17 10:40:22 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo-thunderbird at 4a10c95, checked by a second, independent pass.

Severity: Informational · Category: Authentication/Authorization · Reference: CWE-250 (Execution with Unnecessary Privileges); OWASP A04:2021 (Insecure Design)

Location:

  • src/options.js:60-64
  • src/options.html:17-20
  • src/_locales/en/messages.json:10-21 (tokenHint), :127-130 (lacksCaptures)
  • README.md ("Try it": "tick only captures")
  • core docs/THREAT-MODEL.md:21 (the row "Someone with the token of a mail client")

Issue

The core's threat model bounds a mail client's token tightly:

| Someone with the token of a mail client | Two calls: a brief list of the person's listings, and adding an entry to one listing's history | Scopes: a mail client's token holds listings:bind and nothing else, so it reads a title, a company, a place and a state and never the search, and the worst it can do is add entries the person will read. |

The Thunderbird extension, which is that mail client, instructs the person to make a token with
the captures scope "and nothing more" (options.html:17-20, tokenHint, README), and Test
judges the token by that scope alone:

const canCapture = (me.scopes || []).includes("captures");
...
say(canCapture ? who : `${who} ${t("lacksCaptures")}`, !canCapture);

Nothing in the extension mentions listings:bind. The open feature issue #9 ("File the message
on screen into a listing's history") records the decision "One token or two. Recommended: one
token holding both scopes, since it is one extension on one profile." If that recommendation
lands, the token a Thunderbird profile holds will carry captures and listings:bind together,
and the row's "and nothing else" and its worst case ("add entries the person will read") will no
longer describe it: the same token also fills the review queue and, where pages are kept, sends
renderings (the browser-extension row's consequences). Today no call of either scope is made, so
no promise is broken yet; the two documents already point different ways, and #9 is about to pick
one.

Impact

None today. Once #1 and #9 land under one token, a token read out of a Thunderbird profile
(which is stored unencrypted, see the issue on storage.local) reaches both the capture API and
the listings history, more than the threat model tells an operator to expect from a mail
client's token.

Fix

Decide before #9 lands, and make the documents agree:

  • either keep the threat model's row and have the extension ask for two tokens (one
    captures, one listings:bind), each named for its use, with Test reporting on each;
  • or take #9's recommendation and rewrite the row in postulo/postulo's docs/THREAT-MODEL.md
    to say that the Thunderbird extension's token holds captures and listings:bind, with the
    union of the two rows' worst cases, and change tokenHint, lacksCaptures and the README to
    name both scopes.

Done when THREAT-MODEL.md's mail-client row and the extension's tokenHint/README name the same
scope set, and test/i18n.test.mjs's "the scope keeps its name" test covers every scope named.

#9 (open) is the feature and lists "one token or two" as a decision to take; it does not record
that one token holding both scopes contradicts the core's threat-model row, nor that the current
hint and README name only captures. #1 (open) is the capture feature, which will use captures.

Found on 2026-10-16 in a read-only security review of `postulo-thunderbird` at `4a10c95`, checked by a second, independent pass. **Severity:** Informational · **Category:** Authentication/Authorization · **Reference:** CWE-250 (Execution with Unnecessary Privileges); OWASP A04:2021 (Insecure Design) **Location:** - `src/options.js:60-64` - `src/options.html:17-20` - `src/_locales/en/messages.json:10-21` (`tokenHint`), `:127-130` (`lacksCaptures`) - `README.md` ("Try it": "tick only `captures`") - core `docs/THREAT-MODEL.md:21` (the row "Someone with the token of a mail client") ## Issue The core's threat model bounds a mail client's token tightly: > | **Someone with the token of a mail client** | Two calls: a brief list of the person's listings, and adding an entry to one listing's history | Scopes: a mail client's token holds `listings:bind` and nothing else, so it reads a title, a company, a place and a state and never the search, and the worst it can do is add entries the person will read. | The Thunderbird extension, which is that mail client, instructs the person to make a token with the `captures` scope "and nothing more" (`options.html:17-20`, `tokenHint`, README), and *Test* judges the token by that scope alone: ```js const canCapture = (me.scopes || []).includes("captures"); ... say(canCapture ? who : `${who} ${t("lacksCaptures")}`, !canCapture); ``` Nothing in the extension mentions `listings:bind`. The open feature issue #9 ("File the message on screen into a listing's history") records the decision "One token or two. Recommended: one token holding both scopes, since it is one extension on one profile." If that recommendation lands, the token a Thunderbird profile holds will carry `captures` and `listings:bind` together, and the row's "and nothing else" and its worst case ("add entries the person will read") will no longer describe it: the same token also fills the review queue and, where pages are kept, sends renderings (the browser-extension row's consequences). Today no call of either scope is made, so no promise is broken yet; the two documents already point different ways, and #9 is about to pick one. ## Impact None today. Once #1 and #9 land under one token, a token read out of a Thunderbird profile (which is stored unencrypted, see the issue on `storage.local`) reaches both the capture API and the listings history, more than the threat model tells an operator to expect from a mail client's token. ## Fix Decide before #9 lands, and make the documents agree: - either keep the threat model's row and have the extension ask for **two tokens** (one `captures`, one `listings:bind`), each named for its use, with *Test* reporting on each; - or take #9's recommendation and **rewrite the row** in postulo/postulo's `docs/THREAT-MODEL.md` to say that the Thunderbird extension's token holds `captures` and `listings:bind`, with the union of the two rows' worst cases, and change `tokenHint`, `lacksCaptures` and the README to name both scopes. Done when `THREAT-MODEL.md`'s mail-client row and the extension's `tokenHint`/README name the same scope set, and `test/i18n.test.mjs`'s "the scope keeps its name" test covers every scope named. ## Related #9 (open) is the feature and lists "one token or two" as a decision to take; it does not record that one token holding both scopes contradicts the core's threat-model row, nor that the current hint and README name only `captures`. #1 (open) is the capture feature, which will use `captures`.
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-thunderbird#14
No description provided.