@reblu/site-blocks 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +12980 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -11
- package/dist/index.d.ts +125 -11
- package/dist/index.js +12994 -59
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
package/dist/index.d.cts
CHANGED
|
@@ -34,24 +34,138 @@ declare function buildShareUrls(url: string, title: string): ShareUrls;
|
|
|
34
34
|
* Blog `content` arrives as raw Markdown (tiptap-markdown). This module owns the
|
|
35
35
|
* single, hardened conversion every SITE reuses instead of reinventing it:
|
|
36
36
|
*
|
|
37
|
-
* Markdown ──(marked, GFM)──▶ HTML ──(
|
|
37
|
+
* Markdown ──(marked, GFM)──▶ HTML ──(hast-util-sanitize)──▶ safe HTML
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
* `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but
|
|
41
|
-
*
|
|
39
|
+
* Sanitisation is MANDATORY (XSS): the output is later parsed into React nodes by
|
|
40
|
+
* `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but `sanitizeHtml` is also used
|
|
41
|
+
* directly against `dangerouslySetInnerHTML` for tenant-authored HTML — the nexus
|
|
42
|
+
* "HTML personalizado" editor (`apps/base-api/src/components/nexus/pages/page-editor/
|
|
43
|
+
* page-editor-form.tsx`) writes `page.customHtml`/`section.contentHtml`, rendered in
|
|
44
|
+
* `apps/demo-site/src/app/[slug]/page.tsx` and `apps/demo-site/src/app/services/
|
|
45
|
+
* [slug]/page.tsx` — so the string must be safe on its own regardless of how a
|
|
46
|
+
* consumer chooses to render it.
|
|
42
47
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
48
|
+
* SCOPE OF THE PARITY GUARANTEES BELOW. Everything this module says about preserving a
|
|
49
|
+
* tenant's `class`, `style`, `id`, `hidden`, `data-*` and `aria-*` is a guarantee about
|
|
50
|
+
* the STRING `sanitizeHtml` returns, and it reaches the page unchanged only on the
|
|
51
|
+
* `dangerouslySetInnerHTML` consumers listed above. `MarkdownRenderer` re-parses that
|
|
52
|
+
* string through `html-react-parser` into a token-styled component map
|
|
53
|
+
* (`markdown-renderer.tsx` `buildOptions`), and that map rebuilds most elements from
|
|
54
|
+
* scratch — so on the blog-body path author attributes are dropped downstream of this
|
|
55
|
+
* module regardless of what the schema allows. Widening the renderer's attribute
|
|
56
|
+
* forwarding is a separate change; do not read these comments as a claim about it.
|
|
57
|
+
*
|
|
58
|
+
* REB-606: this used to run on `isomorphic-dompurify`, which pulls `jsdom` into the
|
|
59
|
+
* import graph. `jsdom`'s transitive `html-encoding-sniffer` → `@exodus/bytes` chain
|
|
60
|
+
* is ESM-only and fails to load under Vercel's Node runtime (`ERR_REQUIRE_ESM`),
|
|
61
|
+
* producing a 500 on every route that renders tenant HTML.
|
|
62
|
+
*
|
|
63
|
+
* The replacement is deliberately assembled from the three `hast` utilities DIRECTLY
|
|
64
|
+
* (`parse5` → `hast-util-from-parse5` → `hast-util-sanitize` → `hast-util-to-html`)
|
|
65
|
+
* rather than through the `unified`/`rehype-*` plugin wrappers. `unified().processSync`
|
|
66
|
+
* builds a `VFile`, and `VFile`'s constructor calls `process.cwd()` through `vfile`'s
|
|
67
|
+
* `minproc`/`minpath`/`minurl` modules — which a bundler resolves under the **node**
|
|
68
|
+
* condition, so the bundle acquires bare `path`/`process`/`url` imports. That would
|
|
69
|
+
* reproduce REB-606's exact shape (a build-invisible, runtime-only module failure) in
|
|
70
|
+
* a different runtime: a SITE importing this framework-agnostic entry into a client
|
|
71
|
+
* component gets `Module not found: Can't resolve 'path'`, and an Edge route gets a
|
|
72
|
+
* `TypeError` the first time `sanitizeHtml` reaches `process.cwd`. The direct
|
|
73
|
+
* utilities never construct a `VFile`, so the emitted bundle has NO runtime import
|
|
74
|
+
* outside this package's declared dependencies. `packages/site-blocks/test/
|
|
75
|
+
* use-client-smoke.test.ts` pins that specifier set for both emitted entries — that
|
|
76
|
+
* assertion, not this comment, is what keeps it true.
|
|
77
|
+
*
|
|
78
|
+
* `hast-util-sanitize`'s own `defaultSchema` is GitHub's markdown-comment policy, which
|
|
79
|
+
* is meaningfully STRICTER than DOMPurify's unconfigured default (no `class`, no
|
|
80
|
+
* `style`, no `target`, structural tags like `figure`/`article`/`nav` missing, no
|
|
81
|
+
* `tel:` links, and it rewrites `id`/`name` to fight DOM clobbering). Swapping the
|
|
82
|
+
* library is not "behaviour preserving" if it also silently swaps the policy — a
|
|
83
|
+
* tenant's `customHtml` authored under the old, permissive DOMPurify default would
|
|
84
|
+
* lose styling, anchors, phone CTAs and structure on this deploy. `SANITIZE_SCHEMA`
|
|
85
|
+
* below restores DOMPurify's default HTML profile (see `dompurify/dist/purify.cjs.js`
|
|
86
|
+
* `html$1`/`html`/`IS_ALLOWED_URI`) for everything that carries **no new injection
|
|
87
|
+
* surface**, and it turns GitHub's `id`/`name` clobber-rewrite off (DOMPurify never did
|
|
88
|
+
* this, so leaving it on would break tenant anchor links that worked for the last
|
|
89
|
+
* month).
|
|
90
|
+
*
|
|
91
|
+
* It deliberately does NOT restore the parts of DOMPurify's default that ARE
|
|
92
|
+
* meaningfully riskier and have no evidenced use in this codebase: the `<style>`
|
|
93
|
+
* element, form controls (`form`/`input`/`button`/`select`/`textarea`/`label`/
|
|
94
|
+
* `fieldset`/`legend`/`option`/`optgroup`/`datalist`/`output`/`progress`/`meter`
|
|
95
|
+
* beyond the existing GFM task-list checkbox — phishing/UI-redress surface),
|
|
96
|
+
* `<script>`/`<template>`/`<object>`/`<embed>`/`<canvas>`, image maps (`map`/`area`),
|
|
97
|
+
* document/shadow-DOM plumbing (`html`/`head`/`body`/`slot`/`content`/`shadow`/
|
|
98
|
+
* `element`/`decorator`), the obsolete set (`dir`/`marquee`/`nobr`/`spacer`/`blink`/
|
|
99
|
+
* `menuitem`), `dialog`/`menu`/`search`, and the SVG/MathML namespaces (recurring
|
|
100
|
+
* source of real-world DOMPurify/sanitiser bypasses via `foreignObject`/namespace
|
|
101
|
+
* confusion). The changeset for this release enumerates every one of them.
|
|
102
|
+
*
|
|
103
|
+
* `<style>` specifically: it is not restored because a tenant-authored stylesheet
|
|
104
|
+
* applies to the WHOLE document, not just the injected fragment — attribute-selector
|
|
105
|
+
* rules like `input[value^="a"]{background:url(...)}` read and exfiltrate values from
|
|
106
|
+
* markup this sanitiser never saw. The `style` ATTRIBUTE is a different, narrower
|
|
107
|
+
* primitive (it can only style the element that carries it), it is part of DOMPurify's
|
|
108
|
+
* default that tenant `customHtml` has relied on for a month, and it is kept — see the
|
|
109
|
+
* changeset, which discloses that an inline `style` can still issue an outbound
|
|
110
|
+
* `url()` request. Closing that is a separate, deliberate decision (a CSS property
|
|
111
|
+
* allowlist) this bug-fix does not make.
|
|
112
|
+
*
|
|
113
|
+
* ARIA (`role` + the standard `aria-*` set, enumerated in `RESTORED_ARIA_ATTRIBUTES`
|
|
114
|
+
* below using `property-information`'s exact camelCase spellings — e.g.
|
|
115
|
+
* `ariaValueMax`, not `ariaValuemax`; hast-util-sanitize matches property names
|
|
116
|
+
* case-sensitively, so a naive hyphen-to-camelCase guess silently drops the
|
|
117
|
+
* attribute instead of erroring) is restored too — DOMPurify allows it by default
|
|
118
|
+
* (`ALLOW_ARIA_ATTR`) and losing it would be an accessibility regression on tenant
|
|
119
|
+
* content, not a security fix (see REB-286's axe gate). Custom `data-*` attributes
|
|
120
|
+
* (DOMPurify's `ALLOW_DATA_ATTR`) are restored via hast-util-sanitize's `'data*'`
|
|
121
|
+
* wildcard property name (`lib/index.js:758` in `hast-util-sanitize`) — they carry
|
|
122
|
+
* no URL/script/CSS interpretation, so allowing arbitrary values is safe.
|
|
123
|
+
*
|
|
124
|
+
* `srcset` is the one attribute this schema takes AWAY from DOMPurify's default, on
|
|
125
|
+
* purpose. `protocols` validates only the attribute's leading scheme
|
|
126
|
+
* (`hast-util-sanitize`'s `safeProtocol` looks at the text before the first
|
|
127
|
+
* `:`/`/`/`?`/`#`), but `srcset` is a comma-separated candidate list — so a value like
|
|
128
|
+
* `"https://ok.png 1x, //evil.tld/track.png 2x"` cannot be gated by this schema model
|
|
129
|
+
* at all. `defaultSchema` ships `source: ['srcSet']` and keeps `source`/`picture` in
|
|
130
|
+
* `tagNames`, which would leave `srcSet` on `<source>` as the ONLY completely ungated
|
|
131
|
+
* URL sink in the policy: not script execution (browsers do not run `javascript:` from
|
|
132
|
+
* an image source), but an arbitrary tenant-authored third-party request carrying a
|
|
133
|
+
* visitor's IP, UA and Referer. `RESTORED_MEDIA_ATTRIBUTES.source` replaces that list
|
|
134
|
+
* with protocol-gated `src` only.
|
|
135
|
+
*
|
|
136
|
+
* `<iframe>` is the one non-default TAG we allow, and ONLY when its `src` host is on
|
|
137
|
+
* the YouTube allowlist — every other embed (arbitrary iframes) is dropped.
|
|
138
|
+
*
|
|
139
|
+
* FOREIGN CONTENT (SVG/MathML) IS REMOVED BEFORE THE TREE IS EVEN BUILT, and that
|
|
140
|
+
* pre-pass is load-bearing for three separate reasons — see `dropForeignContent`.
|
|
141
|
+
* The policy above already said the SVG and MathML namespaces are not allowed; until
|
|
142
|
+
* this pass existed that was implemented by UNWRAPPING them, which is a different
|
|
143
|
+
* thing and got two of the three wrong.
|
|
45
144
|
*/
|
|
46
145
|
/** Hosts permitted for `<iframe>` embeds. Exact host match — no suffix tricks. */
|
|
47
146
|
declare const ALLOWED_YOUTUBE_HOSTS: readonly string[];
|
|
48
147
|
/** True when `src` is an absolute URL whose host is exactly a YouTube host. */
|
|
49
148
|
declare function isAllowedYouTubeHost(src: string | null | undefined): boolean;
|
|
50
149
|
/**
|
|
51
|
-
* Sanitise an already-HTML string with the same policy (default-
|
|
52
|
-
* host-gated `<iframe>`). Use for content that arrives as
|
|
53
|
-
* `section.contentHtml` from base-api, which is `marked` output
|
|
54
|
-
* sanitised server-side.
|
|
150
|
+
* Sanitise an already-HTML string with the same policy (DOMPurify-default-equivalent
|
|
151
|
+
* tags/attributes plus a host-gated `<iframe>`). Use for content that arrives as
|
|
152
|
+
* HTML — e.g. page `section.contentHtml` from base-api, which is `marked` output
|
|
153
|
+
* that has NOT been sanitised server-side.
|
|
154
|
+
*
|
|
155
|
+
* WHAT A PARSE FAILURE DOES, AND WHY. Everything up to `fromParse5` runs BEFORE the
|
|
156
|
+
* sanitiser, so a throw there cannot be caught by anything downstream — and none of
|
|
157
|
+
* this module's callers catch anything at all. There are only two behaviours that keep
|
|
158
|
+
* the function's contract ("the returned string is safe to inject as HTML"):
|
|
159
|
+
* propagate, or return `''`. Propagating turns one malformed block inside one tenant's
|
|
160
|
+
* page into a 500 for the WHOLE route, which is the exact incident this module exists
|
|
161
|
+
* to end. So it fails closed: the block is dropped, the rest of the page renders, and
|
|
162
|
+
* the failure is reported through `console.error` so it is not silent — a dropped
|
|
163
|
+
* block with a log line is recoverable, a 500 is not. Returning the input, escaped or
|
|
164
|
+
* otherwise, is not an option: unparsed input is unsanitised input.
|
|
165
|
+
*
|
|
166
|
+
* This is a backstop, not the fix. The known crash class (foreign-content `<template>`)
|
|
167
|
+
* is removed structurally by `dropForeignContent`; this catch exists because
|
|
168
|
+
* `fromParse5` has already surprised us once.
|
|
55
169
|
*/
|
|
56
170
|
declare function sanitizeHtml(html: string | null | undefined): string;
|
|
57
171
|
/** Convert raw Markdown to sanitised, XSS-safe HTML. */
|
|
@@ -60,7 +174,7 @@ declare function markdownToSafeHtml(markdown: string | null | undefined): string
|
|
|
60
174
|
/**
|
|
61
175
|
* `MarkdownRenderer` — the canonical Blog body renderer.
|
|
62
176
|
*
|
|
63
|
-
* Pipeline: raw Markdown → `markdownToSafeHtml` (marked →
|
|
177
|
+
* Pipeline: raw Markdown → `markdownToSafeHtml` (marked → hast-util-sanitize, XSS-safe) →
|
|
64
178
|
* `html-react-parser` into a token-styled React tree. We parse the sanitised HTML
|
|
65
179
|
* into real React nodes (a component map) rather than using
|
|
66
180
|
* `dangerouslySetInnerHTML`, so images lazy-load, external links are hardened, and
|
package/dist/index.d.ts
CHANGED
|
@@ -34,24 +34,138 @@ declare function buildShareUrls(url: string, title: string): ShareUrls;
|
|
|
34
34
|
* Blog `content` arrives as raw Markdown (tiptap-markdown). This module owns the
|
|
35
35
|
* single, hardened conversion every SITE reuses instead of reinventing it:
|
|
36
36
|
*
|
|
37
|
-
* Markdown ──(marked, GFM)──▶ HTML ──(
|
|
37
|
+
* Markdown ──(marked, GFM)──▶ HTML ──(hast-util-sanitize)──▶ safe HTML
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
* `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but
|
|
41
|
-
*
|
|
39
|
+
* Sanitisation is MANDATORY (XSS): the output is later parsed into React nodes by
|
|
40
|
+
* `MarkdownRenderer` (no `dangerouslySetInnerHTML`), but `sanitizeHtml` is also used
|
|
41
|
+
* directly against `dangerouslySetInnerHTML` for tenant-authored HTML — the nexus
|
|
42
|
+
* "HTML personalizado" editor (`apps/base-api/src/components/nexus/pages/page-editor/
|
|
43
|
+
* page-editor-form.tsx`) writes `page.customHtml`/`section.contentHtml`, rendered in
|
|
44
|
+
* `apps/demo-site/src/app/[slug]/page.tsx` and `apps/demo-site/src/app/services/
|
|
45
|
+
* [slug]/page.tsx` — so the string must be safe on its own regardless of how a
|
|
46
|
+
* consumer chooses to render it.
|
|
42
47
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
48
|
+
* SCOPE OF THE PARITY GUARANTEES BELOW. Everything this module says about preserving a
|
|
49
|
+
* tenant's `class`, `style`, `id`, `hidden`, `data-*` and `aria-*` is a guarantee about
|
|
50
|
+
* the STRING `sanitizeHtml` returns, and it reaches the page unchanged only on the
|
|
51
|
+
* `dangerouslySetInnerHTML` consumers listed above. `MarkdownRenderer` re-parses that
|
|
52
|
+
* string through `html-react-parser` into a token-styled component map
|
|
53
|
+
* (`markdown-renderer.tsx` `buildOptions`), and that map rebuilds most elements from
|
|
54
|
+
* scratch — so on the blog-body path author attributes are dropped downstream of this
|
|
55
|
+
* module regardless of what the schema allows. Widening the renderer's attribute
|
|
56
|
+
* forwarding is a separate change; do not read these comments as a claim about it.
|
|
57
|
+
*
|
|
58
|
+
* REB-606: this used to run on `isomorphic-dompurify`, which pulls `jsdom` into the
|
|
59
|
+
* import graph. `jsdom`'s transitive `html-encoding-sniffer` → `@exodus/bytes` chain
|
|
60
|
+
* is ESM-only and fails to load under Vercel's Node runtime (`ERR_REQUIRE_ESM`),
|
|
61
|
+
* producing a 500 on every route that renders tenant HTML.
|
|
62
|
+
*
|
|
63
|
+
* The replacement is deliberately assembled from the three `hast` utilities DIRECTLY
|
|
64
|
+
* (`parse5` → `hast-util-from-parse5` → `hast-util-sanitize` → `hast-util-to-html`)
|
|
65
|
+
* rather than through the `unified`/`rehype-*` plugin wrappers. `unified().processSync`
|
|
66
|
+
* builds a `VFile`, and `VFile`'s constructor calls `process.cwd()` through `vfile`'s
|
|
67
|
+
* `minproc`/`minpath`/`minurl` modules — which a bundler resolves under the **node**
|
|
68
|
+
* condition, so the bundle acquires bare `path`/`process`/`url` imports. That would
|
|
69
|
+
* reproduce REB-606's exact shape (a build-invisible, runtime-only module failure) in
|
|
70
|
+
* a different runtime: a SITE importing this framework-agnostic entry into a client
|
|
71
|
+
* component gets `Module not found: Can't resolve 'path'`, and an Edge route gets a
|
|
72
|
+
* `TypeError` the first time `sanitizeHtml` reaches `process.cwd`. The direct
|
|
73
|
+
* utilities never construct a `VFile`, so the emitted bundle has NO runtime import
|
|
74
|
+
* outside this package's declared dependencies. `packages/site-blocks/test/
|
|
75
|
+
* use-client-smoke.test.ts` pins that specifier set for both emitted entries — that
|
|
76
|
+
* assertion, not this comment, is what keeps it true.
|
|
77
|
+
*
|
|
78
|
+
* `hast-util-sanitize`'s own `defaultSchema` is GitHub's markdown-comment policy, which
|
|
79
|
+
* is meaningfully STRICTER than DOMPurify's unconfigured default (no `class`, no
|
|
80
|
+
* `style`, no `target`, structural tags like `figure`/`article`/`nav` missing, no
|
|
81
|
+
* `tel:` links, and it rewrites `id`/`name` to fight DOM clobbering). Swapping the
|
|
82
|
+
* library is not "behaviour preserving" if it also silently swaps the policy — a
|
|
83
|
+
* tenant's `customHtml` authored under the old, permissive DOMPurify default would
|
|
84
|
+
* lose styling, anchors, phone CTAs and structure on this deploy. `SANITIZE_SCHEMA`
|
|
85
|
+
* below restores DOMPurify's default HTML profile (see `dompurify/dist/purify.cjs.js`
|
|
86
|
+
* `html$1`/`html`/`IS_ALLOWED_URI`) for everything that carries **no new injection
|
|
87
|
+
* surface**, and it turns GitHub's `id`/`name` clobber-rewrite off (DOMPurify never did
|
|
88
|
+
* this, so leaving it on would break tenant anchor links that worked for the last
|
|
89
|
+
* month).
|
|
90
|
+
*
|
|
91
|
+
* It deliberately does NOT restore the parts of DOMPurify's default that ARE
|
|
92
|
+
* meaningfully riskier and have no evidenced use in this codebase: the `<style>`
|
|
93
|
+
* element, form controls (`form`/`input`/`button`/`select`/`textarea`/`label`/
|
|
94
|
+
* `fieldset`/`legend`/`option`/`optgroup`/`datalist`/`output`/`progress`/`meter`
|
|
95
|
+
* beyond the existing GFM task-list checkbox — phishing/UI-redress surface),
|
|
96
|
+
* `<script>`/`<template>`/`<object>`/`<embed>`/`<canvas>`, image maps (`map`/`area`),
|
|
97
|
+
* document/shadow-DOM plumbing (`html`/`head`/`body`/`slot`/`content`/`shadow`/
|
|
98
|
+
* `element`/`decorator`), the obsolete set (`dir`/`marquee`/`nobr`/`spacer`/`blink`/
|
|
99
|
+
* `menuitem`), `dialog`/`menu`/`search`, and the SVG/MathML namespaces (recurring
|
|
100
|
+
* source of real-world DOMPurify/sanitiser bypasses via `foreignObject`/namespace
|
|
101
|
+
* confusion). The changeset for this release enumerates every one of them.
|
|
102
|
+
*
|
|
103
|
+
* `<style>` specifically: it is not restored because a tenant-authored stylesheet
|
|
104
|
+
* applies to the WHOLE document, not just the injected fragment — attribute-selector
|
|
105
|
+
* rules like `input[value^="a"]{background:url(...)}` read and exfiltrate values from
|
|
106
|
+
* markup this sanitiser never saw. The `style` ATTRIBUTE is a different, narrower
|
|
107
|
+
* primitive (it can only style the element that carries it), it is part of DOMPurify's
|
|
108
|
+
* default that tenant `customHtml` has relied on for a month, and it is kept — see the
|
|
109
|
+
* changeset, which discloses that an inline `style` can still issue an outbound
|
|
110
|
+
* `url()` request. Closing that is a separate, deliberate decision (a CSS property
|
|
111
|
+
* allowlist) this bug-fix does not make.
|
|
112
|
+
*
|
|
113
|
+
* ARIA (`role` + the standard `aria-*` set, enumerated in `RESTORED_ARIA_ATTRIBUTES`
|
|
114
|
+
* below using `property-information`'s exact camelCase spellings — e.g.
|
|
115
|
+
* `ariaValueMax`, not `ariaValuemax`; hast-util-sanitize matches property names
|
|
116
|
+
* case-sensitively, so a naive hyphen-to-camelCase guess silently drops the
|
|
117
|
+
* attribute instead of erroring) is restored too — DOMPurify allows it by default
|
|
118
|
+
* (`ALLOW_ARIA_ATTR`) and losing it would be an accessibility regression on tenant
|
|
119
|
+
* content, not a security fix (see REB-286's axe gate). Custom `data-*` attributes
|
|
120
|
+
* (DOMPurify's `ALLOW_DATA_ATTR`) are restored via hast-util-sanitize's `'data*'`
|
|
121
|
+
* wildcard property name (`lib/index.js:758` in `hast-util-sanitize`) — they carry
|
|
122
|
+
* no URL/script/CSS interpretation, so allowing arbitrary values is safe.
|
|
123
|
+
*
|
|
124
|
+
* `srcset` is the one attribute this schema takes AWAY from DOMPurify's default, on
|
|
125
|
+
* purpose. `protocols` validates only the attribute's leading scheme
|
|
126
|
+
* (`hast-util-sanitize`'s `safeProtocol` looks at the text before the first
|
|
127
|
+
* `:`/`/`/`?`/`#`), but `srcset` is a comma-separated candidate list — so a value like
|
|
128
|
+
* `"https://ok.png 1x, //evil.tld/track.png 2x"` cannot be gated by this schema model
|
|
129
|
+
* at all. `defaultSchema` ships `source: ['srcSet']` and keeps `source`/`picture` in
|
|
130
|
+
* `tagNames`, which would leave `srcSet` on `<source>` as the ONLY completely ungated
|
|
131
|
+
* URL sink in the policy: not script execution (browsers do not run `javascript:` from
|
|
132
|
+
* an image source), but an arbitrary tenant-authored third-party request carrying a
|
|
133
|
+
* visitor's IP, UA and Referer. `RESTORED_MEDIA_ATTRIBUTES.source` replaces that list
|
|
134
|
+
* with protocol-gated `src` only.
|
|
135
|
+
*
|
|
136
|
+
* `<iframe>` is the one non-default TAG we allow, and ONLY when its `src` host is on
|
|
137
|
+
* the YouTube allowlist — every other embed (arbitrary iframes) is dropped.
|
|
138
|
+
*
|
|
139
|
+
* FOREIGN CONTENT (SVG/MathML) IS REMOVED BEFORE THE TREE IS EVEN BUILT, and that
|
|
140
|
+
* pre-pass is load-bearing for three separate reasons — see `dropForeignContent`.
|
|
141
|
+
* The policy above already said the SVG and MathML namespaces are not allowed; until
|
|
142
|
+
* this pass existed that was implemented by UNWRAPPING them, which is a different
|
|
143
|
+
* thing and got two of the three wrong.
|
|
45
144
|
*/
|
|
46
145
|
/** Hosts permitted for `<iframe>` embeds. Exact host match — no suffix tricks. */
|
|
47
146
|
declare const ALLOWED_YOUTUBE_HOSTS: readonly string[];
|
|
48
147
|
/** True when `src` is an absolute URL whose host is exactly a YouTube host. */
|
|
49
148
|
declare function isAllowedYouTubeHost(src: string | null | undefined): boolean;
|
|
50
149
|
/**
|
|
51
|
-
* Sanitise an already-HTML string with the same policy (default-
|
|
52
|
-
* host-gated `<iframe>`). Use for content that arrives as
|
|
53
|
-
* `section.contentHtml` from base-api, which is `marked` output
|
|
54
|
-
* sanitised server-side.
|
|
150
|
+
* Sanitise an already-HTML string with the same policy (DOMPurify-default-equivalent
|
|
151
|
+
* tags/attributes plus a host-gated `<iframe>`). Use for content that arrives as
|
|
152
|
+
* HTML — e.g. page `section.contentHtml` from base-api, which is `marked` output
|
|
153
|
+
* that has NOT been sanitised server-side.
|
|
154
|
+
*
|
|
155
|
+
* WHAT A PARSE FAILURE DOES, AND WHY. Everything up to `fromParse5` runs BEFORE the
|
|
156
|
+
* sanitiser, so a throw there cannot be caught by anything downstream — and none of
|
|
157
|
+
* this module's callers catch anything at all. There are only two behaviours that keep
|
|
158
|
+
* the function's contract ("the returned string is safe to inject as HTML"):
|
|
159
|
+
* propagate, or return `''`. Propagating turns one malformed block inside one tenant's
|
|
160
|
+
* page into a 500 for the WHOLE route, which is the exact incident this module exists
|
|
161
|
+
* to end. So it fails closed: the block is dropped, the rest of the page renders, and
|
|
162
|
+
* the failure is reported through `console.error` so it is not silent — a dropped
|
|
163
|
+
* block with a log line is recoverable, a 500 is not. Returning the input, escaped or
|
|
164
|
+
* otherwise, is not an option: unparsed input is unsanitised input.
|
|
165
|
+
*
|
|
166
|
+
* This is a backstop, not the fix. The known crash class (foreign-content `<template>`)
|
|
167
|
+
* is removed structurally by `dropForeignContent`; this catch exists because
|
|
168
|
+
* `fromParse5` has already surprised us once.
|
|
55
169
|
*/
|
|
56
170
|
declare function sanitizeHtml(html: string | null | undefined): string;
|
|
57
171
|
/** Convert raw Markdown to sanitised, XSS-safe HTML. */
|
|
@@ -60,7 +174,7 @@ declare function markdownToSafeHtml(markdown: string | null | undefined): string
|
|
|
60
174
|
/**
|
|
61
175
|
* `MarkdownRenderer` — the canonical Blog body renderer.
|
|
62
176
|
*
|
|
63
|
-
* Pipeline: raw Markdown → `markdownToSafeHtml` (marked →
|
|
177
|
+
* Pipeline: raw Markdown → `markdownToSafeHtml` (marked → hast-util-sanitize, XSS-safe) →
|
|
64
178
|
* `html-react-parser` into a token-styled React tree. We parse the sanitised HTML
|
|
65
179
|
* into real React nodes (a component map) rather than using
|
|
66
180
|
* `dangerouslySetInnerHTML`, so images lazy-load, external links are hardened, and
|