Skip to content

fix(reader): enforce CSP in rendered foliate documents#794

Open
zachyale wants to merge 3 commits intodevelopfrom
bugfix/epub-reader-csp-hardening
Open

fix(reader): enforce CSP in rendered foliate documents#794
zachyale wants to merge 3 commits intodevelopfrom
bugfix/epub-reader-csp-hardening

Conversation

@zachyale
Copy link
Copy Markdown
Member

@zachyale zachyale commented Apr 23, 2026

Description

Restores EPUB reader usability on iOS Safari while keeping script execution blocked for rendered book content.

This changes the Foliate reader back to using allow-scripts in the iframe sandbox so Safari can properly handle reader tap and touch interactions again, and moves the script-blocking control into the actual rendered Foliate documents by injecting a Content-Security-Policy meta tag before they are serialized to blob: URLs

Linked Issue: Fixes #793

Changes

  • restore allow-scripts in Foliate paginator and fixed-layout iframe sandboxes
  • inject Content-Security-Policy: script-src 'none' into rendered EPUB/MOBI documents in epub.js + mobi.js
  • document the Safari/WebKit limitation in the Foliate iframe sandbox comments

Testing

  • Performed a red/green test using sample EPUBs that use scripted content to verify no regression in blocking scripts. Test coverage included sample EPUBs w/ scripted content in the body, as well as files with no head and scripted content within an SVG.

Summary by CodeRabbit

  • Security

    • Rendered book content now includes a Content Security Policy that blocks script execution, preventing embedded scripts from running.
  • Compatibility

    • Improved Safari and iframe interaction by adjusting sandbox behavior while preserving script blocking via the injected CSP.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds helpers that ensure parsed EPUB/MOBI documents have <html> and <head> elements and injects a CSP <meta http-equiv="Content-Security-Policy" content="script-src 'none'"> into serialized XHTML/HTML/SVG outputs. Iframe sandboxing is relaxed to include allow-scripts so browser interactions (e.g., iOS Safari) work while scripts remain blocked.

Changes

Cohort / File(s) Summary
Document normalization & CSP injection
frontend/src/assets/foliate/epub.js, frontend/src/assets/foliate/mobi.js
Add helpers that guarantee an <html> root and <head> element when missing and prepend a CSP <meta http-equiv="Content-Security-Policy" content="script-src 'none'"> to serialized section documents after resource/link replacements.
Iframe sandbox policy
frontend/src/assets/foliate/fixed-layout.js, frontend/src/assets/foliate/paginator.js
Change iframe sandbox attribute from allow-same-origin to allow-same-origin allow-scripts and update inline comments to note CSP will prevent script execution instead of relying solely on sandbox restrictions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through broken heads and seams,
I nest a tag where silence gleams,
No scripts shall leap, but frames can play—
Tap paths return and find their way.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows the conventional commit format with 'fix' type and descriptive subject line about enforcing CSP in Foliate documents.
Description check ✅ Passed The PR description includes all required template sections: Description, Linked Issue, and Changes. Testing section provides additional context demonstrating thoroughness.
Linked Issues check ✅ Passed The code changes directly address issue #793 by restoring allow-scripts in sandbox attributes [fixed-layout.js, paginator.js] while maintaining script blocking through CSP meta tag injection [epub.js, mobi.js].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the issue objective: iframe sandbox restoration for Safari compatibility and CSP injection for script blocking in Foliate documents.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/epub-reader-csp-hardening
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch bugfix/epub-reader-csp-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/assets/foliate/mobi.js (1)

1183-1202: ⚠️ Potential issue | 🔴 Critical

KF8/AZW3 loadSection does not inject the script-blocking CSP — script execution re-enabled for modern Kindle books.

MOBI6.loadSection (line 877) calls prependScriptBlockingMetaCsp(doc) before serialization, but KF8.loadSection here serializes and creates the blob URL without ever injecting the CSP meta. Because the paginator and fixed-layout iframes now carry allow-scripts, any <script> (inline or external) present in a KF8/AZW3 document will now execute in the rendered iframe. This is the same threat model the EPUB/MOBI6 paths address and undoes the PR's security goal for this format.

🔒 Proposed fix
     async loadSection(section) {
         if (this.#cache.has(section)) return this.#cache.get(section)
         const str = await this.loadText(section)
         const replaced = await this.replaceResources(str)
 
         // by default, type is XHTML; change to HTML if it's not valid XHTML
         let doc = this.parser.parseFromString(replaced, this.#type)
         if (doc.querySelector('parsererror') || !doc.documentElement?.namespaceURI) {
             this.#type = MIME.HTML
             doc = this.parser.parseFromString(replaced, this.#type)
         }
         for (const [url, node] of this.#inlineMap) {
             for (const el of doc.querySelectorAll(`img[src="${url}"]`))
                 el.replaceWith(node)
         }
+        // Keep rendered book documents non-scriptable even when Safari requires
+        // allow-scripts on the iframe for parent-side interaction.
+        prependScriptBlockingMetaCsp(doc)
         const url = URL.createObjectURL(
             new Blob([this.serializer.serializeToString(doc)], { type: this.#type }))
         this.#cache.set(section, url)
         return url
     }

Note: since KF8 documents are often XHTML (this.#type === MIME.XHTML), the helper here that uses a plain createElement may produce a non-namespaced <meta> in an XHTML tree. Consider mirroring the createElementNS(documentElement.namespaceURI, ...) variant used in epub.js.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/assets/foliate/mobi.js` around lines 1183 - 1202, loadSection is
missing the call that injects the script-blocking CSP meta so KF8/AZW3 documents
can run scripts; call the same helper used elsewhere
(prependScriptBlockingMetaCsp) on the parsed document before serialization and
blob creation, taking care to create the meta in the document namespace when
this.#type === MIME.XHTML (i.e., use
createElementNS(document.documentElement.namespaceURI, 'meta') or mirror the
epub.js namespaced variant) so the meta is valid in XHTML trees, then
serialize/Blob as before.
♻️ Duplicate comments (1)
frontend/src/assets/foliate/epub.js (1)

111-120: ⚠️ Potential issue | 🟠 Major

Consider removing the existing-CSP short-circuit (same concern as mobi.js).

See the detailed discussion on frontend/src/assets/foliate/mobi.js:16-24. A book-supplied permissive <meta http-equiv="Content-Security-Policy"> will currently cause your restrictive CSP to be skipped; since multiple CSP metas intersect per spec, unconditionally prepending is safer. The createElementNS fallback here is a nice touch for XHTML — worth mirroring to the mobi.js helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/assets/foliate/epub.js` around lines 111 - 120, The function
prependScriptBlockingMetaCsp currently short-circuits if a meta CSP already
exists; remove that short-circuit so the function always prepends a restrictive
meta, i.e., delete or disable the check using
head.querySelector('meta[http-equiv="Content-Security-Policy"]') in
prependScriptBlockingMetaCsp so the new meta (created with createElementNS
fallback) is always prepended to head; mirror the same behavior change you
applied/will apply in mobi.js if present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/assets/foliate/mobi.js`:
- Around line 16-24: The current early-return in prependScriptBlockingMetaCsp
prevents injecting a script-blocking CSP when a permissive CSP meta already
exists; change prependScriptBlockingMetaCsp to always create and prepend a meta
http-equiv="Content-Security-Policy" content="script-src 'none'" into the
document head (if head exists) instead of returning when any
meta[http-equiv="Content-Security-Policy"] is present, and apply the same change
to the near-duplicate helper in frontend/src/assets/foliate/epub.js so both
functions unconditionally prepend the restrictive meta (redundant strict CSPs
are acceptable and safer than skipping injection).

---

Outside diff comments:
In `@frontend/src/assets/foliate/mobi.js`:
- Around line 1183-1202: loadSection is missing the call that injects the
script-blocking CSP meta so KF8/AZW3 documents can run scripts; call the same
helper used elsewhere (prependScriptBlockingMetaCsp) on the parsed document
before serialization and blob creation, taking care to create the meta in the
document namespace when this.#type === MIME.XHTML (i.e., use
createElementNS(document.documentElement.namespaceURI, 'meta') or mirror the
epub.js namespaced variant) so the meta is valid in XHTML trees, then
serialize/Blob as before.

---

Duplicate comments:
In `@frontend/src/assets/foliate/epub.js`:
- Around line 111-120: The function prependScriptBlockingMetaCsp currently
short-circuits if a meta CSP already exists; remove that short-circuit so the
function always prepends a restrictive meta, i.e., delete or disable the check
using head.querySelector('meta[http-equiv="Content-Security-Policy"]') in
prependScriptBlockingMetaCsp so the new meta (created with createElementNS
fallback) is always prepended to head; mirror the same behavior change you
applied/will apply in mobi.js if present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 537f301f-430f-4463-9c09-9ae558b3a6a9

📥 Commits

Reviewing files that changed from the base of the PR and between f21cf1a and 5be3063.

📒 Files selected for processing (4)
  • frontend/src/assets/foliate/epub.js
  • frontend/src/assets/foliate/fixed-layout.js
  • frontend/src/assets/foliate/mobi.js
  • frontend/src/assets/foliate/paginator.js
📜 Review details
🔇 Additional comments (3)
frontend/src/assets/foliate/paginator.js (1)

242-246: Sandbox relaxation is consistent with CSP-based enforcement.

Adding allow-scripts here is fine because rendered documents reaching this iframe have already been processed by epub.js/mobi.js which inject <meta http-equiv="Content-Security-Policy" content="script-src 'none'"> prior to blob URL creation. The linked WebKit bug reference is helpful. Note the KF8 path in mobi.js (see other comment) — that flow currently skips the CSP injection and would undermine this iframe relaxation for AZW3/KF8 books.

frontend/src/assets/foliate/epub.js (1)

937-942: Injection point is correct for the HTML/XHTML/SVG replacement branch.

Placing prependScriptBlockingMetaCsp(doc) after style/resource rewrites and before serialization ensures the CSP meta is present on the blob-served document. Note SVG root documents won't have a <head> so the helper no-ops for pure SVG — acceptable since SVG loaded as a standalone document is less common in spines, but worth being aware of.

frontend/src/assets/foliate/fixed-layout.js (1)

84-87: Sandbox change OK for EPUB fixed-layout; KF8 fixed-layout still at risk.

For EPUB, section load() routes through Loader.loadReplaced in epub.js, so the CSP meta is present before this iframe loads the blob. However, fixed-layout KF8/AZW3 books route through KF8.loadSection in mobi.js, which does not currently inject the CSP meta (see comment on frontend/src/assets/foliate/mobi.js:1183-1202). Once that gap is fixed, this sandbox change is safe across formats.

Comment thread frontend/src/assets/foliate/mobi.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/assets/foliate/epub.js`:
- Around line 111-118: The SVG branch currently bypasses CSP because
prependScriptBlockingMetaCsp looks for a <head> and returns for image/svg+xml;
update loadReplaced / the SVG handling path (where MIME.SVG and
DOMParser('image/svg+xml') are used) to explicitly sanitize SVG documents before
serialization: detect SVG by MIME.SVG or doc.documentElement.namespaceURI ===
'http://www.w3.org/2000/svg', remove all <script> elements and any element
attributes starting with "on" (e.g., onclick, onload), strip or neutralize
<iframe>/<object> that can execute scripts, and/or ensure the iframe sandbox
used for spine/resources does not include allow-scripts for SVG resources
(functions referenced: prependScriptBlockingMetaCsp, loadReplaced, MIME.SVG);
implement these removals prior to serialization so scripted SVGs cannot execute.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e1a5c2e-53e9-4240-b112-eb54cec3481d

📥 Commits

Reviewing files that changed from the base of the PR and between 5be3063 and a52fe46.

📒 Files selected for processing (2)
  • frontend/src/assets/foliate/epub.js
  • frontend/src/assets/foliate/mobi.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/assets/foliate/mobi.js

Comment thread frontend/src/assets/foliate/epub.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/assets/foliate/mobi.js (1)

1203-1222: ⚠️ Potential issue | 🔴 Critical

CSP injection is missing from the KF8 rendering path — scripts will execute for KF8/AZW3 books.

MOBI6.loadSection now injects the script-blocking CSP (line 897), but KF8.loadSection serializes the parsed document straight to a blob: URL at line 1219 without calling prependScriptBlockingMetaCsp. Since paginator.js/fixed-layout.js iframe sandboxes were relaxed to include allow-scripts, any scripts inside a KF8/AZW3 book will execute in the rendered iframe — exactly the regression this PR is meant to prevent. KF8 is the dominant modern MOBI/Kindle format, so the fix must cover it as well as MOBI6.

Also note: KF8.#type defaults to MIME.XHTML (line 986). When injecting into an XHTML document, the <meta> must be created in the document's namespace, matching what epub.js does with createElementNS(doc.documentElement?.namespaceURI, 'meta'). The current prependScriptBlockingMetaCsp helper (line 40) only uses doc.createElement, which yields a no-namespace element in XHTML and will either fail CSP parsing in strict XML or simply be ignored. Suggest aligning this helper with the epub.js version.

🛡️ Proposed fix
 const prependScriptBlockingMetaCsp = doc => {
     const head = ensureHeadElement(doc)
-    const meta = doc.createElement('meta')
+    const nsURI = doc.documentElement?.namespaceURI
+    const meta = nsURI ? doc.createElementNS(nsURI, 'meta') : doc.createElement('meta')
     meta.setAttribute('http-equiv', 'Content-Security-Policy')
     meta.setAttribute('content', "script-src 'none'")
     head.prepend(meta)
 }
     async loadSection(section) {
         if (this.#cache.has(section)) return this.#cache.get(section)
         const str = await this.loadText(section)
         const replaced = await this.replaceResources(str)

         // by default, type is XHTML; change to HTML if it's not valid XHTML
         let doc = this.parser.parseFromString(replaced, this.#type)
         if (doc.querySelector('parsererror') || !doc.documentElement?.namespaceURI) {
             this.#type = MIME.HTML
             doc = this.parser.parseFromString(replaced, this.#type)
         }
         for (const [url, node] of this.#inlineMap) {
             for (const el of doc.querySelectorAll(`img[src="${url}"]`))
                 el.replaceWith(node)
         }
+        // Keep rendered book documents non-scriptable even when Safari requires
+        // allow-scripts on the iframe for parent-side interaction.
+        prependScriptBlockingMetaCsp(doc)
         const url = URL.createObjectURL(
             new Blob([this.serializer.serializeToString(doc)], { type: this.#type }))
         this.#cache.set(section, url)
         return url
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/assets/foliate/mobi.js` around lines 1203 - 1222,
KF8.loadSection is missing the CSP injection step that MOBI6.loadSection has, so
update KF8.loadSection to call prependScriptBlockingMetaCsp(doc) after
parsing/replacing resources and before serializing to a Blob (the code around
the method named loadSection in the KF8 class); also modify the
prependScriptBlockingMetaCsp helper to create the <meta> element in the document
namespace when the parsed doc is XHTML (use
doc.createElementNS(doc.documentElement?.namespaceURI, 'meta') when namespaceURI
exists, otherwise fall back to doc.createElement) so the injected CSP meta is
valid for XML/XHTML documents and will be honored by the browser.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@frontend/src/assets/foliate/mobi.js`:
- Around line 1203-1222: KF8.loadSection is missing the CSP injection step that
MOBI6.loadSection has, so update KF8.loadSection to call
prependScriptBlockingMetaCsp(doc) after parsing/replacing resources and before
serializing to a Blob (the code around the method named loadSection in the KF8
class); also modify the prependScriptBlockingMetaCsp helper to create the <meta>
element in the document namespace when the parsed doc is XHTML (use
doc.createElementNS(doc.documentElement?.namespaceURI, 'meta') when namespaceURI
exists, otherwise fall back to doc.createElement) so the injected CSP meta is
valid for XML/XHTML documents and will be honored by the browser.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cbf05057-59cf-4b3e-91f2-b992f3913ecd

📥 Commits

Reviewing files that changed from the base of the PR and between a52fe46 and e062636.

📒 Files selected for processing (2)
  • frontend/src/assets/foliate/epub.js
  • frontend/src/assets/foliate/mobi.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/assets/foliate/epub.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)

@imnotjames
Copy link
Copy Markdown
Contributor

I confirmed this correctly avoids the exploit in XHTML pages - both when using inline scripts, as script tags, or when referencing external script sources. This still allows SVG or other renderable documents that allow scripting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS Safari EPUB reader touch handling is degraded

2 participants