SEC2-076: Instance address keeps a user name and password typed into it, and they reach the popup footer and the downloadable log #33

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

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

Severity: Low · Category: Input Validation · Reference: CWE-20 (Improper Input Validation); CWE-532 (Insertion of Sensitive Information into Log File); OWASP A04:2021 (Insecure Design)

Location:

  • src/lib/api.js:25-47 (normaliseInstance)
  • src/lib/api.js:50-53 (originPattern)
  • src/popup.js:162-167 (the footer shows the address)
  • src/options.js:49-64 (downloadLog heads the file with settings.instance)

Issue

normaliseInstance parses the typed address with URL, insists on https away from home, drops
the query, the fragment and a trailing /api/v1, and returns url.toString():

url.search = "";
url.hash = "";
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, "");
return url.toString().replace(/\/$/, "");

Nothing looks at url.username or url.password. An address pasted with basic-auth credentials
in it (the form a reverse proxy's documentation often shows) is accepted and stored as the
instance. Three things then follow from the stored string rather than from its origin:

  • originPattern uses url.host, so the permission prompt names the bare host, while every
    request is built as ${instance}${API}${path}; fetch refuses a URL that carries
    credentials, so each call throws before it is sent, and stateOfFailure reads that as
    "unreachable". The person sees Postulo as down, with no sentence naming the cause.
  • The popup footer writes settings.instance into server.textContent, server.title and
    server.href (src/popup.js:164-166), so the password stands in the popup in clear.
  • downloadLog writes postulo: settings.instance as the first lines of the log file
    (src/options.js:60). PRIVACY.md:28-31 promises that the log never holds a credential, and
    log.js records hostOf(url) only; the header line put in front of it does not.

Impact

A person who pastes a proxy address with its credentials gets a working-looking configuration
that never connects, a popup that shows the password, and a log file ("the kind of file that
gets attached to an issue", src/lib/log.js:6) carrying it. Limited to the person's own browser
and whoever they send the log to.

Fix

Refuse credentials in the address with a translated sentence, and head the log with the host
only:

// src/lib/api.js, in normaliseInstance after `url = new URL(text)`
if (url.username || url.password) {
  throw new Refusal("noCredentialsInAddress");
}
// src/options.js, in downloadLog
postulo: settings.instance ? hostOf(settings.instance) : "(not set up)",

Add the key to src/_locales/en/messages.json (the other catalogues through Weblate).
src/lib/api.js is one of the files copied into postulo-thunderbird through shared.json, so
the fix reaches that extension on its next shared:sync.

Done when test/api.test.mjs asserts that normaliseInstance("https://user:secret@postulo.example.org")
throws the new refusal, and a test of the log text with the options page's header shows the host
and nothing after an @.

#19 (closed) is the non-JSON-answer sentence, unrelated.

Found on 2026-10-16 in a read-only security review of `postulo-chromium` at `b6c1009`, checked by a second, independent pass. **Severity:** Low · **Category:** Input Validation · **Reference:** CWE-20 (Improper Input Validation); CWE-532 (Insertion of Sensitive Information into Log File); OWASP A04:2021 (Insecure Design) **Location:** - `src/lib/api.js:25-47` (`normaliseInstance`) - `src/lib/api.js:50-53` (`originPattern`) - `src/popup.js:162-167` (the footer shows the address) - `src/options.js:49-64` (`downloadLog` heads the file with `settings.instance`) ## Issue `normaliseInstance` parses the typed address with `URL`, insists on https away from home, drops the query, the fragment and a trailing `/api/v1`, and returns `url.toString()`: ```js url.search = ""; url.hash = ""; url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/, ""); return url.toString().replace(/\/$/, ""); ``` Nothing looks at `url.username` or `url.password`. An address pasted with basic-auth credentials in it (the form a reverse proxy's documentation often shows) is accepted and stored as the instance. Three things then follow from the stored string rather than from its origin: - `originPattern` uses `url.host`, so the permission prompt names the bare host, while every request is built as `${instance}${API}${path}`; `fetch` refuses a URL that carries credentials, so each call throws before it is sent, and `stateOfFailure` reads that as "unreachable". The person sees Postulo as down, with no sentence naming the cause. - The popup footer writes `settings.instance` into `server.textContent`, `server.title` and `server.href` (`src/popup.js:164-166`), so the password stands in the popup in clear. - `downloadLog` writes `postulo: settings.instance` as the first lines of the log file (`src/options.js:60`). `PRIVACY.md:28-31` promises that the log never holds a credential, and `log.js` records `hostOf(url)` only; the header line put in front of it does not. ## Impact A person who pastes a proxy address with its credentials gets a working-looking configuration that never connects, a popup that shows the password, and a log file ("the kind of file that gets attached to an issue", `src/lib/log.js:6`) carrying it. Limited to the person's own browser and whoever they send the log to. ## Fix Refuse credentials in the address with a translated sentence, and head the log with the host only: ```js // src/lib/api.js, in normaliseInstance after `url = new URL(text)` if (url.username || url.password) { throw new Refusal("noCredentialsInAddress"); } ``` ```js // src/options.js, in downloadLog postulo: settings.instance ? hostOf(settings.instance) : "(not set up)", ``` Add the key to `src/_locales/en/messages.json` (the other catalogues through Weblate). `src/lib/api.js` is one of the files copied into postulo-thunderbird through `shared.json`, so the fix reaches that extension on its next `shared:sync`. Done when `test/api.test.mjs` asserts that `normaliseInstance("https://user:secret@postulo.example.org")` throws the new refusal, and a test of the log text with the options page's header shows the host and nothing after an `@`. ## Related #19 (closed) is the non-JSON-answer sentence, unrelated.
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-chromium#33
No description provided.