Skip to content

Commit 378cefc

Browse files
tjelite1986claude
andcommitted
Document verifyTokenLoose JWT-in-URL tradeoff in READMEs
Background security review flagged verifyTokenLoose for putting bearer JWTs in URLs (leak via access logs, browser history, Referer header from rendered EPUB/PDF content, intermediate proxies). The pattern is deliberate — it exists because plain <img>/<embed>/<iframe>/<a download> tags can't set Authorization headers — and accepted for small self-hosted single-user-base deployments behind a single reverse proxy. Documenting the tradeoff explicitly so future consumers don't take the pattern as "fine": - authentication/README.md: full "known tradeoff" section with the asset-scoped-capability-token migration path (POST asset-token endpoint returns short-lived sub-scoped JWT, asset route verifies res === expected + exp, Referrer-Policy: no-referrer). - bookshelf/README.md: shorter security note pointing to the authentication docs, plus a concrete callout that EPUB/PDF content is exactly the kind of payload that can leak the token-bearing URL via Referer. Code unchanged in this commit — see authentication 0.3.0 / bookshelf 0.2.0 if/when the scoped-token migration is taken on. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a966d7c commit 378cefc

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

authentication/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,29 @@ On lockout the route responds `429` with a `Retry-After` header. A successful lo
2626

2727
The identifier is lowercased before hashing into the table, so `Alice` and `alice` share a bucket — that matches how the user lookup works (email is lowercased; usernames are case-sensitive but a brute-forcer doesn't know that).
2828

29+
## `verifyTokenLoose` — known tradeoff
30+
31+
`verifyTokenLoose(req)` accepts the JWT either in the `Authorization` header OR in a `?t=<jwt>` query parameter. It exists because browsers can't set custom headers on plain `<img src>`, `<video src>`, `<embed src>`, `<iframe src>`, or `<a download href>`, and asset routes (`bookshelf` covers/files, photo-gallery thumbnails, etc.) need *some* way to authenticate those requests.
32+
33+
**The tradeoff:** putting a bearer JWT in a URL means it can leak via:
34+
35+
- HTTP access logs (web server, reverse proxy, CDN)
36+
- Browser history and tab-restore state
37+
- The `Referer` header on any outbound request the rendered asset triggers (especially an EPUB or PDF that contains external links)
38+
- Intermediate proxy caches
39+
40+
In this module's threat model — small self-hosted apps behind a single Traefik reverse proxy, single user-base, HTTPS-only — this is an accepted risk. The token is the same long-lived (30 day) JWT the rest of the API uses, so a leaked URL is roughly equivalent to a stolen session.
41+
42+
**If you need a tighter bound**, replace `verifyTokenLoose` with a short-lived asset-scoped capability token:
43+
44+
1. Add an endpoint `POST /api/<resource>/:id/asset-token` that takes the bearer JWT in the `Authorization` header
45+
2. Have it return `jwt.sign({ sub: user.id, res: 'book:<slug>/file', exp: now + 300 }, ASSET_SECRET)`
46+
3. In the asset route, verify the scoped token and check `payload.res === expectedResource` and `payload.exp`
47+
4. Set `Referrer-Policy: no-referrer` on asset responses to suppress the `Referer` leak from rendered content
48+
5. Configure your access-log scrubbers to strip the `t` query param
49+
50+
This is a real improvement but a non-trivial refactor — each asset route gains a one-RTT prelude and the client has to manage the asset-token lifecycle. Consider it for production deployments that face untrusted networks or share an environment with logging infrastructure you don't control.
51+
2952
## Installation
3053

3154
### 1. Install npm deps

bookshelf/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ Then set `BOOKS_ROOT` and `BOOK_COVERS_DIR` in `.env`, mount the former as a hos
8787
- `@/lib/bookStorage``BOOKS_ROOT`, `BOOK_COVERS_DIR`, `bookFilePath`, `bookCoverPath`, `detectFormat`, `ensureBookDirs`, `slugify`, `uniqueSlug`
8888
- `@/lib/bookCovers``extractCover`
8989

90+
## Security note: bearer JWT in `?t=` query
91+
92+
The cover and file routes use `verifyTokenLoose`, which accepts the full session JWT in a `?t=` query parameter so that plain `<img>`, `<embed>`, `<iframe>` and `<a download>` tags can authenticate without setting custom headers. This is the same long-lived (30-day) JWT used for the rest of the API.
93+
94+
URLs leak — through access logs, browser history, the `Referer` header (especially significant here: EPUB and PDF readers render embedded content that can trigger outbound requests), and intermediate proxies. The book itself is the perfect attack surface for `Referer`-based leakage.
95+
96+
For small self-hosted single-user-base deployments behind a single reverse proxy, this is accepted in the [authentication module](../authentication/README.md). For anything tighter (multi-tenant, untrusted networks, regulated workloads), follow the asset-scoped-capability-token migration path documented there before exposing this module publicly.
97+
98+
At minimum, regardless of deployment, you should set `Referrer-Policy: no-referrer` on cover and file responses so an embedded link inside the rendered book can't leak the token-bearing URL. The current routes do not do this — patch them in your fork if you ship to anyone you don't trust.
99+
90100
## Known gotchas
91101

92102
- `epubjs` is **pinned to `0.3.93`** — versions `0.4.x` reintroduce a legacy `xmldom` dependency with a critical CVE. Don't let your lockfile drift.

0 commit comments

Comments
 (0)