SEC2-087: The API token sits in storage.local in clear, outside Thunderbird's primary password, and the privacy text does not say so #16

Closed
opened 2026-10-17 10:40:24 +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: Data Exposure · Reference: CWE-312 (Cleartext Storage of Sensitive Information); OWASP A02:2021 (Cryptographic Failures)

Location:

  • src/lib/thunderbird.js:8-15
  • README.md ("Privacy, as it stands")
  • src/_locales/en/messages.json:10-12 (tokenHint: "It is kept in this Thunderbird profile only.")

Issue

The bearer token is written and read as a plain string:

export async function loadSettings() {
  const stored = await ext.storage.local.get(["instance", "token"]);
  return { instance: stored.instance || "", token: stored.token || "" };
}

export async function saveSettings({ instance, token }) {
  await ext.storage.local.set({ instance, token });
}

storage.local is a per-extension IndexedDB file inside the profile folder. Thunderbird's
primary password encrypts the accounts' mail passwords in logins.json, not extension storage,
so the Postulo token is readable by anything that can read the profile folder (another process of
the same user, a profile backup, a synced folder) even when a primary password is set. The README
and tokenHint say the token "stays in this Thunderbird profile", which is true, but a person who
has set a primary password may read that as "protected like my mail passwords". No WebExtension
API offers encrypted storage, so this is a documentation point, not a code defect; the
compensating controls are the token's narrow scope and revocation from Settings.

The token is otherwise handled well: it travels only in the Authorization header to
endpoint(instance, ...) (api.js:57-65, 161-164), never in a URL, never logged (no console.*
call in src/), and only to the origin the person granted (options.js:44, 56,
thunderbird.js:28-30).

Impact

Someone with read access to the profile folder holds a working captures token (and, once #9
lands, perhaps a listings:bind one as well) until the person revokes it. Scoped and revocable,
so limited; recorded so that the privacy text matches the storage.

Fix

In the README's "Privacy, as it stands" (and the PRIVACY.md that #1 plans) add one sentence: the
token is stored unencrypted in the profile's extension storage, Thunderbird's primary password
does not cover it, and anyone who can read the profile folder can use it until it is revoked
under Settings → API tokens. Optionally, add a "Forget token" button on the options page that
clears it from storage.local (today the only way is to overwrite it).

Done when the README says so and, if the control is added, a test asserts that the options page
has it and that pressing it leaves no token in storage.

#8 (open) mentions storage.local for the display-language setting only. The scope question for
this same token is the issue on the mail-client row of the threat model.

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:** Data Exposure · **Reference:** CWE-312 (Cleartext Storage of Sensitive Information); OWASP A02:2021 (Cryptographic Failures) **Location:** - `src/lib/thunderbird.js:8-15` - `README.md` ("Privacy, as it stands") - `src/_locales/en/messages.json:10-12` (`tokenHint`: "It is kept in this Thunderbird profile only.") ## Issue The bearer token is written and read as a plain string: ```js export async function loadSettings() { const stored = await ext.storage.local.get(["instance", "token"]); return { instance: stored.instance || "", token: stored.token || "" }; } export async function saveSettings({ instance, token }) { await ext.storage.local.set({ instance, token }); } ``` `storage.local` is a per-extension IndexedDB file inside the profile folder. Thunderbird's primary password encrypts the accounts' mail passwords in `logins.json`, not extension storage, so the Postulo token is readable by anything that can read the profile folder (another process of the same user, a profile backup, a synced folder) even when a primary password is set. The README and `tokenHint` say the token "stays in this Thunderbird profile", which is true, but a person who has set a primary password may read that as "protected like my mail passwords". No WebExtension API offers encrypted storage, so this is a documentation point, not a code defect; the compensating controls are the token's narrow scope and revocation from Settings. The token is otherwise handled well: it travels only in the `Authorization` header to `endpoint(instance, ...)` (`api.js:57-65, 161-164`), never in a URL, never logged (no `console.*` call in `src/`), and only to the origin the person granted (`options.js:44, 56`, `thunderbird.js:28-30`). ## Impact Someone with read access to the profile folder holds a working `captures` token (and, once #9 lands, perhaps a `listings:bind` one as well) until the person revokes it. Scoped and revocable, so limited; recorded so that the privacy text matches the storage. ## Fix In the README's "Privacy, as it stands" (and the PRIVACY.md that #1 plans) add one sentence: the token is stored unencrypted in the profile's extension storage, Thunderbird's primary password does not cover it, and anyone who can read the profile folder can use it until it is revoked under *Settings → API tokens*. Optionally, add a "Forget token" button on the options page that clears it from `storage.local` (today the only way is to overwrite it). Done when the README says so and, if the control is added, a test asserts that the options page has it and that pressing it leaves no `token` in storage. ## Related #8 (open) mentions `storage.local` for the display-language setting only. The scope question for this same token is the issue on the mail-client row of the threat model.
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#16
No description provided.