SEC2-078: Requests that carry the bearer token follow redirects #35

Closed
opened 2026-10-17 10:40:13 +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: Informational · Category: Data Exposure · Reference: CWE-522 (Insufficiently Protected Credentials); OWASP A07:2021 (Identification and Authentication Failures)

Location:

  • src/lib/api.js:166-174 (request)
  • src/lib/api.js:64-75 (headers), src/lib/api.js:177-180, 199-217, 223-233, 241-246, 260-277 (the callers)

Issue

async function request(fetchImpl, url, init) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
  try {
    return await fetchImpl(url, { ...init, signal: controller.signal });

No redirect option is given, so fetch follows redirects (redirect: "follow" is the default)
for GET /me, POST /captures, GET /captures, POST /captures/known and the PUT of a
rendering, each carrying Authorization: Bearer …. Current browsers drop the Authorization
header when a redirect crosses origins, so in practice the token stays on the instance's origin;
the extension itself does not make that so, and a same-origin redirect carries the token and the
page to any path the instance's front end chooses. PRIVACY.md:47-49 promises the token goes "to
the address you configured, and to nothing else".

Impact

Defence in depth only: it takes the instance, its reverse proxy, or the network path of a plain
http://…local instance to issue the redirect, and a browser that still forwards Authorization
across origins.

Fix

Make the promise the extension's own: pass redirect: "error" in request (a redirect then
throws, is read as "no answer" and the capture waits, which is the right outcome for a
misconfigured proxy), or redirect: "manual" and treat an opaque-redirect response as
unreachable.

Done when test/api.test.mjs asserts that the init handed to fetchImpl carries
redirect: "error" for every call that carries the token.

Found on 2026-10-16 in a read-only security review of `postulo-chromium` at `b6c1009`, checked by a second, independent pass. **Severity:** Informational · **Category:** Data Exposure · **Reference:** CWE-522 (Insufficiently Protected Credentials); OWASP A07:2021 (Identification and Authentication Failures) **Location:** - `src/lib/api.js:166-174` (`request`) - `src/lib/api.js:64-75` (`headers`), `src/lib/api.js:177-180`, `199-217`, `223-233`, `241-246`, `260-277` (the callers) ## Issue ```js async function request(fetchImpl, url, init) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), TIMEOUT_MS); try { return await fetchImpl(url, { ...init, signal: controller.signal }); ``` No `redirect` option is given, so `fetch` follows redirects (`redirect: "follow"` is the default) for `GET /me`, `POST /captures`, `GET /captures`, `POST /captures/known` and the `PUT` of a rendering, each carrying `Authorization: Bearer …`. Current browsers drop the `Authorization` header when a redirect crosses origins, so in practice the token stays on the instance's origin; the extension itself does not make that so, and a same-origin redirect carries the token and the page to any path the instance's front end chooses. `PRIVACY.md:47-49` promises the token goes "to the address you configured, and to nothing else". ## Impact Defence in depth only: it takes the instance, its reverse proxy, or the network path of a plain `http://…local` instance to issue the redirect, and a browser that still forwards `Authorization` across origins. ## Fix Make the promise the extension's own: pass `redirect: "error"` in `request` (a redirect then throws, is read as "no answer" and the capture waits, which is the right outcome for a misconfigured proxy), or `redirect: "manual"` and treat an opaque-redirect response as `unreachable`. Done when `test/api.test.mjs` asserts that the `init` handed to `fetchImpl` carries `redirect: "error"` for every call that carries the token.
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#35
No description provided.