# CMP-9 — Content authored by one user and rendered to another is sanitised at the render boundary; author-time schema constraints are not sufficient

L1 · hybrid · Components & patterns

**Fails when:** dangerouslySetInnerHTML/v-html renders another user's authored content with no sanitiser in the render path · sanitisation is claimed at editor time only ("schema-constrained output") with nothing enforced at render · a prototype defers sanitisation with no recorded flag noting the deferral

## Requirement

When content authored by one user is rendered to a different user, the HTML must be
sanitised (e.g. an allowlist sanitiser such as DOMPurify) before render. The guarantee
must hold at the render boundary — the point where the content actually reaches the
other user's screen — not only at author time. An editor's output being
"schema-constrained" when it was written is not sufficient on its own; a schema
constraint can drift, be bypassed, or simply not cover every field that later gets
rendered raw.

## Rationale

No control covered untrusted-HTML rendering across a trust boundary before this one.
The triggering evidence: staff-authored rich-text comments rendered to external readers via
`dangerouslySetInnerHTML` (`src/components/reports/report-preview.tsx` in the consumer
repo), labelled in-code "schema-constrained Tiptap output (prototype)" — a claim made
at author time, with nothing enforcing it at the render boundary where it actually
matters. The same pattern was also noted for an announcements composer. This surfaced
as an UNCOVERED finding across two separate evaluator passes: a ship-blocking security
gap the catalog was silent on, not a style preference.

## Passes when

- Content authored by one user and rendered to a different user passes through an
  allowlist sanitiser immediately before render.
- The sanitisation guarantee is enforced at the render boundary, not asserted only from
  the editor's schema constraints.
- A mock-data prototype defers sanitisation only with an explicit, recorded flag noting
  the deferral and that it must be resolved before any real-user-content ship.

## Fails when

- `dangerouslySetInnerHTML` or `v-html` renders another user's authored content with no
  sanitiser call anywhere in the render path.
- Sanitisation is claimed only at editor/author time ("schema-constrained output") with
  nothing enforced at render.
- A prototype defers sanitisation with no recorded flag — silent deferral, not a
  documented one.

## Evaluator guidance

Two halves, one hybrid check:

1. **Deterministic sub-check** — `checks/cmp-scan.py`, planned in #158, not yet built;
   manual until it ships: search the changed surface for `dangerouslySetInnerHTML` / `v-html`
   or equivalent, and identify whether the content it renders originates from a
   *different* user than the one viewing it. If so, confirm a sanitiser call sits in
   the render path (not merely at the editor).
2. **Judgment sub-check**: read the render boundary directly — the component or
   function that actually outputs HTML to the screen — and confirm the sanitisation
   guarantee holds there. An in-code comment claiming "schema-constrained" is not
   evidence of render-time sanitisation; find the actual sanitiser call, or the
   finding stands.

Cite the specific file and render call in every finding, per this catalog's evidence
requirement.

## Do not flag

- Content authored and rendered back to the *same* user — no trust boundary is
  crossed, so this control does not apply.
- A mock-data prototype whose sanitisation deferral is explicitly flagged in the
  decision record — the flag is the control working as intended.
- Sanitisation implemented via a render-time allowlist regardless of which library
  performs it (DOMPurify is the named example, not a requirement to use that specific
  library).
