docs: correct the CSP entry in TODO.md

Scripted EPUBs are a regression from the foliate migration, not a pre-existing
gap: epub.js sandboxed without allow-scripts by default, foliate sets it
unconditionally. Records grimmory's fix — CSP on per-entry responses rather
than app-wide — which also sidesteps the mode-watcher blocker, plus the
whole-file buffering it would remove.
This commit is contained in:
2026-08-12 14:21:10 -04:00
parent 7e04826fa5
commit 5305d3bb5e
+63 -18
View File
@@ -51,28 +51,73 @@ Worth adding at the same time:
## Frontend ## Frontend
### No CSP, so scripted EPUBs run against the app origin ### Scripted EPUBs run against the app origin
EPUB files may contain JavaScript. foliate-js renders each section in an iframe from a **This is a regression from the foliate-js migration, not a pre-existing gap.**
**same-origin** `blob:` URL and cannot sandbox it — `allow-scripts` is required, and
blob URLs inherit the embedder's origin — so script inside a book can reach `/api/*`
with the session cookie attached. foliate's own README says not to use it without a
Content Security Policy blocking scripts.
The obvious policy is `kit.csp` in `svelte.config.js` with `script-src: ['self']`, and The old epub.js reader never passed `allowScriptedContent`. epub.js defaults it to
deliberately no `default-src` (it would also cover `style-src`/`img-src`/`font-src` and `false`, which sets `iframe.sandbox = "allow-same-origin"` — no `allow-scripts` — so
kill both the book's own blob: assets and the inline `<style id="chitai-theme">` that script inside a book never ran. The vendored foliate-js sets, unconditionally:
`hooks.server.ts` injects via `transformPageChunk`).
**What blocks it:** `mode-watcher` renders its own inline `setInitialMode` script, which ```js
sets the dark class before first paint. SvelteKit only nonces the bootstrap script it // paginator.js — and the same in fixed-layout.js
injects itself, so that one is blocked and every page load flashes the light theme. // `allow-scripts` is needed for events because of WebKit bug
Fixing it means pinning a SHA-256 of a third-party inline script whose contents change this.#iframe.setAttribute("sandbox", "allow-same-origin allow-scripts");
with the package version and the props passed — it would break silently on upgrade, and ```
the symptom would be a theme flash rather than an error.
Worth revisiting if `mode-watcher` gains a nonce prop, or if the theme class moves to a `allow-same-origin` together with `allow-scripts` is the combination that makes the
cookie so the server can set it without an inline script. sandbox attribute do nothing. Sections are served as same-origin `blob:` URLs, so script
in a book can reach `/api/*` with the session cookie attached. foliate's README says as
much and tells you to use a CSP instead; we have not added one.
This is not theoretical. Audiobookshelf shipped the same combination and got
**CVE-2024-35236** — scripted EPUB plus an unrestricted upload gave remote code
execution; fixed in 2.10.0 by making scripted content a per-library opt-in, off by
default. Kavita (CVE-2024-39307) and Jellyfin (fixed 10.9.8) are variations on it.
Write-up: <https://gebir.ge/blog/every-trick-in-the-book/>.
**An app-wide CSP is the wrong shape.** `kit.csp` with `script-src: ['self']` also blocks
`mode-watcher`'s inline `setInitialMode`, which sets the dark class before first paint —
SvelteKit only nonces the bootstrap script it injects itself, so every page load would
flash the light theme. Pinning a hash of a third-party inline script breaks silently on
upgrade.
**Grimmory solves it properly**, and it runs foliate-js too. Rather than handing foliate
the whole file, it serves each EPUB entry from its own endpoint and puts the strict
policy on that response:
```java
// EpubReaderController.java
response.setHeader("Content-Security-Policy", "script-src 'none'");
```
The app shell keeps its own, more permissive policy. That works because each section is
then a real same-origin document with its own header, rather than a `blob:` — and a
`blob:` inherits the CSP of the document that created it, which is exactly why a header
on `/api/books/download/…` would achieve nothing today.
Two ways forward:
1. **Cheap.** Patch the vendored `sandbox` attribute to drop `allow-scripts`, restoring
what epub.js gave us. Cost is the WebKit bug the upstream comment cites: events inside
the iframe get swallowed, which would likely break touch/swipe paging and possibly the
in-iframe keyboard handling in `foliate-view.svelte`. Needs testing before trusting.
2. **Right.** Follow grimmory: a backend endpoint serving individual EPUB entries with
`script-src 'none'`, and drive foliate through its `{ loadText, loadBlob, getSize }`
loader hooks instead of a whole-file blob.
Option 2 also fixes the memory cost below, which is why it is worth more than it looks.
### Book files are buffered whole, twice
`frontend/src/routes/api/[...path]/+server.ts` reads every response with
`await response.arrayBuffer()` and forwards no `Range` header, so opening a book pulls
the entire file into the node process and then again into the browser. A large art PDF
or a comic archive is tens of megabytes each time, per reader.
Serving EPUB entries individually (option 2 above) removes it for EPUB. PDFs would still
want range support in the proxy, which is what the vendored pdf.js viewer expects and
currently never gets.
### Remove the epub.js locations-cache purge ### Remove the epub.js locations-cache purge