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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user