mirror of
https://github.com/goauthentik/authentik.git
synced 2026-06-17 19:09:11 +03:00
web/elements/ak-mdx: sanitize replacer output, note pipeline drift
Address PR review feedback on the URL-mode trust boundary. `<ak-mdx>`'s `replacers` hook runs over pre-rendered build-time HTML before it is stamped into the DOM, and consumers (e.g. `ProxyProviderViewPage`) splice admin-controlled values such as `provider.externalHost` into it. The old React pipeline ran replacers on raw markdown that was then compiled, so those values were HTML-escaped on serialization; the new URL mode passed the post-replacer HTML straight through, dropping that guarantee. Replace the passthrough `CompiledMarkdownTrustPolicy` with `CompiledMarkdownSanitizePolicy`: a DOMPurify policy that whitelists the custom elements (`<ak-alert>`, `<ak-md-a>`, `<ak-diagram>`) and the `part`/`level` attributes our pipeline emits, and strips anything else a replacer could inject. Also add a reciprocal drift note to the runtime `markdown.ts` pointing at `bundler/mdx-plugin/`, mirroring the existing note on the bundler side. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<ak-md-a>` and `<ak-alert>` 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 `<ak-mdx>` in URL mode.
|
||||
*
|
||||
* The compiled HTML originates from source we own, but `<ak-mdx>` 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 (`<ak-alert>`, `<ak-md-a>`, `<ak-diagram>`) 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"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 (`<ak-alert>`, `<ak-md-a>`) 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
|
||||
* (`<ak-alert>`, `<ak-md-a>`, `<ak-diagram>`) while stripping anything
|
||||
* a replacer could have injected.
|
||||
*/
|
||||
async #hydrateFromURL(url: string): Promise<SlottedTemplateResult> {
|
||||
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),
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
* (`<ak-alert>`, `<ak-md-a>`, `<ak-diagram>`) 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:
|
||||
|
||||
Reference in New Issue
Block a user