diff --git a/web/src/common/purify.ts b/web/src/common/purify.ts index 0421b4eebc..afd8238c18 100644 --- a/web/src/common/purify.ts +++ b/web/src/common/purify.ts @@ -51,14 +51,30 @@ export const SanitizedTrustPolicy = trustedTypes.createPolicy("authentik-sanitiz }); /** - * Trusted types policy for HTML produced by our own build-time markdown - * pipeline. The HTML is generated from source we own (the `mdx-plugin`), - * including custom elements like `` and `` that - * DOMPurify's default tag list would strip. Treat the input as already - * trusted and pass it through unmodified. + * Trusted types policy for HTML produced by our build-time markdown + * pipeline (`mdx-plugin`) and stamped into `` in URL mode. + * + * The compiled HTML originates from source we own, but `` exposes + * a `replacers` hook that lets consumers splice dynamic — and sometimes + * admin-controlled — values into it before render (e.g. a proxy + * provider's `externalHost` in `ProxyProviderViewPage`). Once a replacer + * has run, "we own every byte" no longer holds, so we re-sanitize here + * rather than passing the string through untouched. + * + * DOMPurify's default tag list would strip the custom elements our + * pipeline emits (``, ``, ``) along with + * the `part`/`level` attributes they rely on, so those are explicitly + * allowed. Everything else — script handlers, unknown elements, unsafe + * URLs injected via a replacer — is still removed. */ -export const CompiledMarkdownTrustPolicy = trustedTypes.createPolicy("authentik-markdown", { - createHTML: (trustedHTML: string) => trustedHTML, +export const CompiledMarkdownSanitizePolicy = trustedTypes.createPolicy("authentik-markdown", { + createHTML: (untrustedHTML: string) => { + return DOMPurify.sanitize(untrustedHTML, { + RETURN_TRUSTED_TYPE: false, + ADD_TAGS: ["ak-alert", "ak-md-a", "ak-diagram"], + ADD_ATTR: ["part", "level"], + }); + }, }); /** diff --git a/web/src/elements/ak-mdx/ak-mdx.ts b/web/src/elements/ak-mdx/ak-mdx.ts index b7cf6e2bb1..01c9fde65c 100644 --- a/web/src/elements/ak-mdx/ak-mdx.ts +++ b/web/src/elements/ak-mdx/ak-mdx.ts @@ -3,7 +3,7 @@ import "#elements/Diagram/ak-diagram"; import "#elements/ak-mdx/components/ak-md-a"; import { globalAK } from "#common/global"; -import { BrandedHTMLPolicy, CompiledMarkdownTrustPolicy, sanitizeHTML } from "#common/purify"; +import { BrandedHTMLPolicy, CompiledMarkdownSanitizePolicy, sanitizeHTML } from "#common/purify"; import { compileRuntimeMarkdown } from "#elements/ak-mdx/markdown"; import Styles from "#elements/ak-mdx/styles.css"; @@ -88,10 +88,13 @@ export class AKMDX extends AKElement { } /** - * URL mode: HTML comes from our build-time pipeline. It may contain - * custom-element tags (``, ``) that DOMPurify's - * default tag list would strip, so we route it through a Trusted - * Types policy that passes the input through unmodified. + * URL mode: HTML comes from our build-time pipeline. `replacers` may + * splice dynamic, sometimes admin-controlled, values into it (see + * `ProxyProviderViewPage`), so the post-replacer string is routed + * through {@linkcode CompiledMarkdownSanitizePolicy} — a DOMPurify + * policy that preserves the custom elements our pipeline emits + * (``, ``, ``) while stripping anything + * a replacer could have injected. */ async #hydrateFromURL(url: string): Promise { const { relBase } = globalAK().api; @@ -107,7 +110,7 @@ export class AKMDX extends AKElement { this.dataset.publicDirectory = module.publicDirectory; } - const trustedHTML = CompiledMarkdownTrustPolicy.createHTML( + const trustedHTML = CompiledMarkdownSanitizePolicy.createHTML( this.#applyReplacers(module.content), ); diff --git a/web/src/elements/ak-mdx/markdown.ts b/web/src/elements/ak-mdx/markdown.ts index 75e6878f2c..d3270d9f9a 100644 --- a/web/src/elements/ak-mdx/markdown.ts +++ b/web/src/elements/ak-mdx/markdown.ts @@ -14,9 +14,19 @@ import { unified } from "unified"; /** * Compile an admin-supplied markdown string to an HTML string in the - * browser. The pipeline is a strict subset of the build-time one: no - * syntax highlighting, no anchor rewriting — the output is plain HTML - * that the existing `BrandedHTMLPolicy` (DOMPurify) sanitizes cleanly. + * browser. The pipeline is a strict subset of the build-time one in + * `bundler/mdx-plugin/compile.js`: no syntax highlighting, no anchor + * rewriting, no mermaid — the output is plain HTML that the existing + * `BrandedHTMLPolicy` (DOMPurify) sanitizes cleanly. + * + * The two pipelines share the remark transforms (admonitions, headings, + * lists) but deliberately diverge on the rehype side: this one stays + * minimal, whereas the build-time one emits custom elements + * (``, ``, ``) that are preserved by the + * URL-mode `CompiledMarkdownSanitizePolicy`. **If you add a transform + * that should apply to both surfaces, update `bundler/mdx-plugin/` + * too** — the shared remark plugins live in `#elements/ak-mdx/remark/*` + * and are mirrored there to keep drift easy to spot. * * Unlike `@mdx-js/mdx`'s `evaluate` / `run`, none of the `unified`, * `remark-*`, or `rehype-*` packages execute the input as JavaScript: