Files
readest/apps/readest-app/src/utils/sanitize.ts
T
Huang Xin 8425d0b91f fix(opds): render HTML in publication descriptions (#4510)
* fix(opds): render HTML in publication descriptions, closes #4503

OPDS publication descriptions showed raw HTML tags (literal `<p>`,
`&quot;`, `&#x27;`) instead of rendering them. Some aggregator feeds
serve the description as an Atom `type="text"` summary whose HTML has
been escaped twice; foliate's getContent only un-escapes `type="html"`/
`"xhtml"`, so the markup survives parsing as entity text and the detail
view dumped it straight into an unsanitized `dangerouslySetInnerHTML`
(also an XSS sink for untrusted feed content).

Add `getOPDSDescriptionHtml`: decode one extra entity level only when the
value is entirely escaped markup (mixed content like `<p>see &lt;code&gt;`
is left literal), then sanitize with the shared DOMPurify sanitizer.
Wire it into PublicationView and render the sanitized HTML.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: consolidate HTML sanitizers into @/utils/sanitize

sanitizeHtml/sanitizeForParsing are generic DOMPurify wrappers, not
specific to Send-to-Readest. Now that OPDS description rendering also
needs sanitizeHtml, move them out of services/send/conversion into the
shared @/utils/sanitize module (alongside sanitizeString) so neither
consumer reaches across the other's feature boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 06:52:34 +02:00

78 lines
2.1 KiB
TypeScript

import DOMPurify from 'dompurify';
export const sanitizeString = (str?: string) => {
if (!str) return str;
return str.replace(/\u0000/g, '');
};
/**
* Strip untrusted HTML (Send-to-Readest email/web/DOCX conversion, OPDS feed
* descriptions, etc.) down to safe, EPUB-appropriate structural markup. Removes
* scripts, event handlers, styles, iframes, and form controls; keeps headings,
* text, lists, tables, links and images.
*
* Runs against the real DOM, so callers must be on the client (browser or Tauri
* webview), both of which provide `window`/`DOMParser`.
*/
export function sanitizeHtml(html: string): string {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'p',
'br',
'hr',
'blockquote',
'pre',
'code',
'strong',
'em',
'b',
'i',
'u',
's',
'sup',
'sub',
'span',
'ul',
'ol',
'li',
'dl',
'dt',
'dd',
'table',
'thead',
'tbody',
'tr',
'th',
'td',
'a',
'img',
'figure',
'figcaption',
],
// `id` is allowed so heading anchors survive — the EPUB's nested
// navMap uses `chapter1.xhtml#heading-id` to link the TOC sidebar to
// each section. Without it readers see one entry per chapter only.
ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'colspan', 'rowspan', 'id'],
// Drop anything that would load or run remote code.
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form', 'input'],
FORBID_ATTR: ['srcset'],
ALLOW_DATA_ATTR: false,
});
}
/**
* Strip scripts and event handlers from an untrusted *document* before it is
* handed to `DOMParser`. Unlike `sanitizeHtml`, this keeps the document
* structure (`<head>`, `<title>`, sectioning elements) so title extraction and
* Readability still work — it only removes anything executable.
*/
export function sanitizeForParsing(html: string): string {
return DOMPurify.sanitize(html, { WHOLE_DOCUMENT: true });
}