WEB / WEB-10.S03
Escaping, sanitization, CSP and Trusted Types
Why are text escaping, formatting policy and typed-sink enforcement different controls?
1. Core · Mechanism
Read every scenario without motion
Vetted rich text
- Local start
The fixed p/class/em tree and atlas-rich-text policy declaration are ready in a local ledger. Formatting projection, revision and sink-type decisions are pending. No actual sanitizer, TrustedHTML instance or browser enforcement runs.
- Local UI path
- Input ready; no render
- Local policy gate
- Gate pending; no approved value
- Local path selection
The UI selects a plain-text or declared rich-text sink path; their interpretation boundaries differ.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Gate pending; no approved value
- Local formatting gate
The fixed tree projection preserves p/em structure and removes class; no actual HTML sanitizer is executed.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Projected tree approved; revision=1
- Local type declaration
The allowed atlas-rich-text policy’s TrustedHTML declaration passes the selected sink gate; no real brand is created.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Declared TrustedHTML accepted
- Expected rich outcome
The UI expects the reviewed revision’s benign p/em tree; neither parsing nor browser TT enforcement is executed.
- Local UI path
- Expected p/em text: Tea & Bread
- Local policy gate
- Declared TrustedHTML accepted
Plain text
- Local start
Tea & Bread selects the h2.textContent string path. This branch will enter neither a rich-formatting parser nor an HTML type gate. Its literal outcome is a local-model expectation, not actual DOM measurement.
- Local UI path
- Input ready; no render
- Local policy gate
- Gate pending; no approved value
- Local path selection
The UI selects a plain-text or declared rich-text sink path; their interpretation boundaries differ.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Gate pending; no approved value
- Local text gate
Ordinary h2.textContent is not an HTML-interpreting sink; this path does not require the selected TrustedHTML type.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Text path allowed; HTML gate irrelevant
- Literal text outcome
The original Tea & Bread literal outcome is expected without pre-encoding or rich-formatting parsing.
- Local UI path
- Expected literal: Tea & Bread
- Local policy gate
- Text path allowed; HTML gate irrelevant
Type mismatch
- Local start
The rich HTML sink input is declared only as a string. The selected supporting/enforce/no-default profile will apply its type check. No approved value or new rich tree exists; no actual browser sink is called.
- Local UI path
- Input ready; no render
- Local policy gate
- Gate pending; no approved value
- Local path selection
The UI selects a plain-text or declared rich-text sink path; their interpretation boundaries differ.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Gate pending; no approved value
- Local enforced type gate
The supporting, enforcing, no-default profile rejects a plain string at the selected innerHTML sink.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Type mismatch denied; no approved value
- DOM unchanged
The UI creates no new rich tree; this harmless declaration produces a local denial with no executable payload.
- Local UI path
- Render unchanged; local denied
- Local policy gate
- Type mismatch denied; no approved value
Policy denied
- Local start
The rich input is declared under other-policy while atlas-rich-text is permitted. Local policy-name checking is still pending. A type label alone establishes neither creation permission nor sanitizer correctness.
- Local UI path
- Input ready; no render
- Local policy gate
- Gate pending; no approved value
- Local path selection
The UI selects a plain-text or declared rich-text sink path; their interpretation boundaries differ.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Gate pending; no approved value
- Local policy-name gate
other-policy is absent from the selected approved list; the policy-creation declaration is rejected.
- Local UI path
- Path selected; sink pending
- Local policy gate
- Policy creation denied; no value
- No rich rendering
Without an approved typed value the UI rendering stays unchanged; this is not an authorization or sanitizer test result.
- Local UI path
- Render unchanged; policy denied
- Local policy gate
- Policy creation denied; no value
ACTORS · STATE · DATA FLOW
Where does autoescaping protect data?
Template autoescaping keeps a plain title as data according to the output grammar; it does not turn every application value into universally trusted output. Choose text rendering by default and review any separate rich-text path. Escape hatches such as React’s raw-HTML prop reintroduce an HTML sink: sharing the same framework does not remove interpretation. Framework URL handling also cannot replace application-specific destination policy. This lesson runs no framework renderer; its ledger separates controls needed for plain text, declared raw HTML and policy-approved rich text.
Sanitization is a formatting contract
If rich text genuinely needs p/em/strong structure, entity encoding alone turns formatting into text. Sanitization constrains parsed content under a selected tag, attribute and URL policy; deleting a few words from a string does not solve HTML grammar. Atlas’s benign p/class/em declaration permits p/em/strong and no attributes, removes class="note" styling and preserves Tea & Bread. This expected tree projection is not an actual HTML sanitizer. Verify the chosen library/configuration, parsing behavior, version and subsequent mutations separately; adding markup afterward changes the scope of prior review.
CSP and Trusted Types are separate gates
CSP script/resource policy restricts execution and loading choices; it does not sanitize untrusted HTML or grant business permission. Report-only monitoring is not enforcement. Trusted Types applies expected-type/policy-conversion gates at selected injection sinks. require-trusted-types-for sink enforcement differs from trusted-types policy-name control. A TrustedHTML brand does not make the browser independently determine that policy content is safe; policy callbacks remain security-critical. This selected profile assumes a supporting browser, enforcement, no default policy and no author-supplied parser options: raw strings fail at the HTML sink and an approved sanitized alias passes. Current draft/implementation coverage and alternate parser-options paths are not generalized from that narrow profile.
2. Red · Trust & Impact
Read every scenario without motion
Brand assumption
- Formatting policy
The input tree has class note on p and an em child. The projected tree retains p/em formatting while removing class; this known-tree transformation is not a full HTML sanitizer.
- Rich-text ledger
- p/em retained / class removed
- Type and policy gate
- Not yet evaluated
- Acceptance scope
- Not yet evaluated
- Typed gate conditions
The normal innerHTML row declares TrustedHTML, atlas-rich-text, revision 1 and policyCreated true. These conditions describe expected gate acceptance; no branded instance is actually created.
- Rich-text ledger
- p/em retained / class removed
- Type and policy gate
- Normal declared gate accepted true
- Acceptance scope
- Not yet evaluated
- Narrow meaning of the brand
A browser brand check does not independently audit the callback data policy. brandMeansSanitized is false; acceptance alone proves neither correct rich-text transformation nor server authority.
- Rich-text ledger
- p/em retained / class removed
- Type and policy gate
- Normal declared gate accepted true
- Acceptance scope
- Declared brand ≠ sanitizer correctness
- Raw-string comparison
type-mismatch supplies a string to innerHTML and expects false under enforce/no-default. Ordinary h2.textContent accepts a string; the two sinks have different type requirements.
- Rich-text ledger
- innerHTML string false / textContent string true
- Type and policy gate
- Normal declared gate accepted true
- Acceptance scope
- Declared brand ≠ sanitizer correctness
- Policy-creation distinction
other-policy has policyCreated false and expected false. Approved name, reviewed revision and correct sink type are separate conditions; observing one does not supply the others.
- Rich-text ledger
- innerHTML string false / textContent string true
- Type and policy gate
- Name/revision/type remain separate
- Acceptance scope
- Declared brand ≠ sanitizer correctness
- Review transformation separately
Plain text remains the default; the rich-text exception needs its own formatting and final-tree review. Type gates, CSP and storage do not replace semantic review.
- Rich-text ledger
- innerHTML string false / textContent string true
- Type and policy gate
- Name/revision/type remain separate
- Acceptance scope
- Type gate ≠ final-tree approval
Later change
- Reviewed tree
Revision 1 projected tree contains allowed p/em with empty attributes. If the result later changes, the earlier review no longer describes the new final tree.
- Rich-text ledger
- Reviewed revision 1 / projected tree
- Type and policy gate
- Not yet evaluated
- Acceptance scope
- Not yet evaluated
- Mutation assumption
postSanitizeMutationRetainsReview is false. Later additions or reinterpretation are not automatically approved by the same policy name; the final-interpreter value needs review.
- Rich-text ledger
- Reviewed revision 1 / projected tree
- Type and policy gate
- Later mutation invalidates prior review scope
- Acceptance scope
- Not yet evaluated
- CSP has another task
The enforced self-source row is matched and not blocked. An allowed script source does not approve rich-text semantics; cspSanitizesHTML remains false.
- Rich-text ledger
- Reviewed revision 1 / projected tree
- Type and policy gate
- Later mutation invalidates prior review scope
- Acceptance scope
- Allowed source ≠ sanitized data
- Report versus enforce
The external source is blocked under enforce but not report; both produce diagnostics. A diagnostic does not imply the same blocking outcome or actual report delivery.
- Rich-text ledger
- Enforce blocks / report only diagnoses
- Type and policy gate
- Later mutation invalidates prior review scope
- Acceptance scope
- Allowed source ≠ sanitized data
- Support and default policy
This profile assumes supportDeclared true, defaultPolicy false and htmlDocument true. Other browsers or default-policy behavior are not generalized from this narrow type table.
- Rich-text ledger
- Enforce blocks / report only diagnoses
- Type and policy gate
- Supported enforce / no default assumed
- Acceptance scope
- Allowed source ≠ sanitized data
- Acceptance boundary
Final tree, correct type, policy creation and enforcement are separate questions. Without appropriate evidence for each, neither whole-site safety nor framework sanitization is concluded.
- Rich-text ledger
- Enforce blocks / report only diagnoses
- Type and policy gate
- Supported enforce / no default assumed
- Acceptance scope
- Separate semantic/type/enforcement evidence
ASSUMPTION · BOUNDARY · CONTROL
Inferring too much from an approved-type label
An allowed policy name does not prove that its createHTML callback sanitizes correctly. A declared TrustedHTML result likewise proves neither site-wide XSS safety nor server authorization. The failed assumption is that a browser brand check independently audits the entire rich-text policy. The browser checks typed values at selected sinks; the application decides which transformation to trust. The artifact therefore keeps input tree, projected tree, policy alias, revision and sink type separate, and explicitly creates no real TrustedHTML instance. Its type declaration belongs to the model scope.
Later mutations are outside earlier controls
Appending raw markup from another pipeline or allowing a library to reinterpret sanitized output changes the reviewed final tree. A CSP header does not certify the mutation, and an ordinary script-src allow decision does not approve data semantics. The raw-string mismatch branch uses harmless fixed markup to show rejection under the selected enforcing/no-default gate, with no executable payload or bypass variant. Report-only mismatches do not give the same blocking result, nor can unsupported browsers be assumed to enforce it. Trusted Types is present is insufficient evidence without policy disposition and feature support.
3. Blue · Secure development & defense
Read every scenario without motion
Type controls
- Normal formatting output
The tree projector compares only the supplied inputTree with allowedTags/allowedAttributes; TT and CSP tables calculate declared type/policy/disposition predicates. Diagnostic=true is neither reportDelivered nor actualExecution. Type mismatch and policy denial have distinct local reasons; this ledger is not a real sanitizer or browser-enforcement producer. Input and projected trees are compared: p and em remain, class is removed, and Tea & Bread text is retained. allowedAttributes is empty; projection handles only the supplied structure.
- Tree and gate ledger
- Text retained / tags retained / attrs empty
- Control comparison
- Not yet evaluated
- Evidence result
- Not yet evaluated
- Four type rows
Normal rich type and plain text expect true; innerHTML string and other-policy expect false. Outcomes are bound to sink/type/policy rather than one trusted label.
- Tree and gate ledger
- Text retained / tags retained / attrs empty
- Control comparison
- Two accepted / two distinct rejects
- Evidence result
- Not yet evaluated
- CSP controls record
Self enforce, external enforced denial and external report continuation are retained separately. reportDelivered and actualExecution remain false; Boolean model results do not change them.
- Tree and gate ledger
- Text retained / tags retained / attrs empty
- Control comparison
- Two accepted / two distinct rejects
- Evidence result
- Source/disposition controls / no execution
- Record revision binding
The reviewed projected tree is associated with atlas-rich-text revision 1. Stored source, approved result and final-sink data are not treated as one object or one piece of evidence.
- Tree and gate ledger
- Source/result/revision/sink distinct
- Control comparison
- Two accepted / two distinct rejects
- Evidence result
- Source/disposition controls / no execution
- Retain native boundary
A finite tree projector can calculate text and attributes without proving HTML-grammar sanitization. actualSanitizer and actualTrustedTypes are false, and no browser brand object exists here.
- Tree and gate ledger
- Source/result/revision/sink distinct
- Control comparison
- Finite projector ≠ sanitizer/TrustedHTML runtime
- Evidence result
- Source/disposition controls / no execution
- Separate regression questions
Controls verify final text/tree, type gate and disposition separately. A correctly displayed ordinary string cannot stand in for sanitizer, CSP and framework success.
- Tree and gate ledger
- Source/result/revision/sink distinct
- Control comparison
- Finite projector ≠ sanitizer/TrustedHTML runtime
- Evidence result
- Separate tree/type/disposition regressions
Type evidence gap
- Policy-name-only view
The tree projector compares only the supplied inputTree with allowedTags/allowedAttributes; TT and CSP tables calculate declared type/policy/disposition predicates. Diagnostic=true is neither reportDelivered nor actualExecution. Type mismatch and policy denial have distinct local reasons; this ledger is not a real sanitizer or browser-enforcement producer. The review sees only atlas-rich-text, without final value, sink or disposition. An approved name alone proves neither creation, callback output nor browser branding.
- Tree and gate ledger
- Name known / value/sink/disposition missing
- Control comparison
- Not yet evaluated
- Evidence result
- Not yet evaluated
- Separate declared result
The normal row declares TrustedHTML while brandedInstanceCreated is false. Copying that declaration as an accepted browser instance invents a runtime result.
- Tree and gate ledger
- Name known / value/sink/disposition missing
- Control comparison
- Declaration ≠ created branded instance
- Evidence result
- Not yet evaluated
- Retain unknown semantics
Sanitizer correctness and final-sink acceptance remain UNKNOWN for the incomplete record. A correct policy name cannot establish success without the final tree or later-mutation context.
- Tree and gate ledger
- Name known / value/sink/disposition missing
- Control comparison
- Declaration ≠ created branded instance
- Evidence result
- Sanitizer/sink acceptance UNKNOWN
- Select required fields
Sink, value type, policy creation, revision, support and enforcement disposition are needed. Rich text also needs final tree and attributes without requiring private bodies.
- Tree and gate ledger
- Needed: tree/type/policy/revision/disposition
- Control comparison
- Declaration ≠ created branded instance
- Evidence result
- Sanitizer/sink acceptance UNKNOWN
- Retain the positive control
Plain text and normal rich output are different positive controls. Raw-string mismatch and denied policy are separate negatives; they do not fill missing browser evidence.
- Tree and gate ledger
- Needed: tree/type/policy/revision/disposition
- Control comparison
- Positive/negative controls retained
- Evidence result
- Sanitizer/sink acceptance UNKNOWN
- Boundary of new evidence
New evidence must answer type gating and transformation separately. This record retains only the policy name and adds no browser-enforcement or sanitizer-success claim.
- Tree and gate ledger
- Needed: tree/type/policy/revision/disposition
- Control comparison
- Positive/negative controls retained
- Evidence result
- Name retained / collect scoped runtime evidence
IMPLEMENTATION · SECURITY CONTROL · VERIFICATION
Default to text and review rich-text exceptions
The tree projector compares only the supplied inputTree with allowedTags/allowedAttributes; TT and CSP tables calculate declared type/policy/disposition predicates. Diagnostic=true is neither reportDelivered nor actualExecution. Type mismatch and policy denial have distinct local reasons; this ledger is not a real sanitizer or browser-enforcement producer. Use an ordinary text API for plain titles and give one explicit renderer ownership of rich-text tag/attribute policy. Choose and pin a maintained sanitizer that understands browser grammar; the lesson’s finite tree projector is not a substitute. The allowed p/em/strong example preserves formatting and removes a disallowed benign class, while the text-only branch never enters a markup parser. Regressions need both expected text and allowed tree/attribute results. Keep source storage values separate from approved render-result revisions.
Record type gates and CSP evidence together
Policy-creation permission, sanitizer output, correct sink type and enforcement disposition are separate checks. The selected ledger permits atlas-rich-text, requires reviewed revision 1 and rejects plain strings in its supporting, enforcing, no-default innerHTML profile. Ordinary h2.textContent does not need this HTML type gate and keeps the original text as data. The CSP matrix separates report-only and enforcement without inventing delivered reports. Native encodings/data projections, pure-model outcomes and actual sanitizer/Trusted-Types/browser behavior are distinct evidence levels. A new browser observation must record feature support, applied policy and sink outcome together; this ledger cannot replace that observation.
NOW APPLY IT TO THE CASE
Does a TrustedHTML label prove the transformation is correct?
Atlas rich-text policy permits p, em and strong tags and expects the class attribute removed. InnerHTML has an atlas-rich-text revision 1 type/policy gate, while textContent accepts ordinary strings. These declared outcomes do not measure a real sanitizer or browser Trusted Types enforcement.
Order response receipt, parsing, callbacks and visible DOM outcome in the case. Try explaining the retained old title through this lesson’s browser-state distinctions.
Open the related case file ↗READ THE EXAMPLE TOGETHER
Rich-text policy and typed-sink ledger
Select a field and examine what it supports and where the evidence ends.
Read the raw artifact and field explanations
{
"profile": {
"plainTitle": "Tea & Bread",
"richInput": "<p class=\"note\">Tea & <em>Bread</em></p>",
"declaredRichOutput": "<p>Tea & <em>Bread</em></p>",
"actualSanitizer": false,
"actualTrustedTypes": false,
"actualBrowser": false,
"actualFramework": false,
"actualScript": false,
"htmlDocument": true,
"authorParserOptions": false,
"defaultPolicy": false,
"supportDeclared": true
},
"formatPolicy": {
"allowedTags": [
"p",
"em",
"strong"
],
"allowedAttributes": [],
"inputTree": {
"tag": "p",
"attrs": {
"class": "note"
},
"children": [
{
"text": "Tea & "
},
{
"tag": "em",
"attrs": {},
"children": [
{
"text": "Bread"
}
]
}
]
},
"projectedTree": {
"tag": "p",
"attrs": {},
"children": [
{
"text": "Tea & "
},
{
"tag": "em",
"attrs": {},
"children": [
{
"text": "Bread"
}
]
}
]
},
"plainTextExpected": "Tea & Bread",
"projectorIsHTMLSanitizer": false
},
"trustedTypes": {
"policyNames": [
"atlas-rich-text"
],
"expectedSinkType": "TrustedHTML",
"sink": "innerHTML",
"enforce": true,
"reviewedRevision": 1,
"brandedInstanceCreated": false,
"rows": [
{
"case": "normal",
"sink": "innerHTML",
"typeDeclared": "TrustedHTML",
"policyName": "atlas-rich-text",
"reviewRevision": 1,
"policyCreated": true,
"expected": true
},
{
"case": "plain-text",
"sink": "h2.textContent",
"typeDeclared": "string",
"policyName": null,
"reviewRevision": null,
"policyCreated": false,
"expected": true
},
{
"case": "type-mismatch",
"sink": "innerHTML",
"typeDeclared": "string",
"policyName": null,
"reviewRevision": null,
"policyCreated": false,
"expected": false
},
{
"case": "policy-denied",
"sink": "innerHTML",
"typeDeclared": "TrustedHTML",
"policyName": "other-policy",
"reviewRevision": 1,
"policyCreated": false,
"expected": false
}
]
},
"csp": {
"sourcePolicy": "script-src 'self'",
"ttPolicy": "require-trusted-types-for 'script'; trusted-types atlas-rich-text",
"documentOrigin": "https://atlas.example.org",
"rows": [
{
"sourceOrigin": "https://atlas.example.org",
"disposition": "enforce",
"matched": true,
"blocked": false,
"diagnostic": false
},
{
"sourceOrigin": "https://static.example.net",
"disposition": "enforce",
"matched": false,
"blocked": true,
"diagnostic": true
},
{
"sourceOrigin": "https://static.example.net",
"disposition": "report",
"matched": false,
"blocked": false,
"diagnostic": true
}
],
"actualRequest": false,
"actualExecution": false,
"reportDelivered": false
},
"boundaries": {
"brandMeansSanitized": false,
"cspSanitizesHTML": false,
"autoescapingCoversRawHTML": false,
"typeMeansServerPermission": false,
"postSanitizeMutationRetainsReview": false
}
}- formatPolicy
- Allowed formatting-tree projection of a fixed benign declaration; not an HTML sanitizer.
- trustedTypes.rows
- Declared type/policy gate for a selected enforcing/no-default profile.
- csp.rows
- Owned self-source matrix; no script fetching/execution or report delivery.
- profile
- Separately declares absence of actual sanitizer/TT/browser/framework evidence.
Finite benign tree projection and declared policy/type outcomes; not actual sanitization, branded TrustedHTML, CSP/TT enforcement, browser/parser or framework execution.
Two reality checks
TrustedHTML is automatically sanitized by the browser.
The type brand carries the application policy’s trust decision; callbacks and final formatting policy must still be correct.
CSP replaces autoescaping and sanitization.
CSP adds execution/loading boundaries; it does not automatically repair output contexts or an allowed rich-text tree.
YOUR TURN
Work through this story.
Predict the outcome, find the evidence boundary, and explain it in your own words.
An enforced Trusted Types condition applies to innerHTML. The input is only a string. What does the type-mismatch row predict?
- It is not accepted; this row has expected=false.
- A public source removes the type requirement.
Read the explained answer
It is not accepted; this row has expected=false. — This profile requires TrustedHTML for innerHTML. h2.textContent accepting a string is a different sink condition.
Why does the same sourceOrigin have different blocked values in enforce and report rows?
Selected lesson artifact fields; teaching example
- csp.rows[1]
- {"sourceOrigin":"https://static.example.net","disposition":"enforce","matched":false,"blocked":true,"diagnostic":true}
- csp.rows[2]
- {"sourceOrigin":"https://static.example.net","disposition":"report","matched":false,"blocked":false,"diagnostic":true}
- csp.reportDelivered
- false
- Any diagnostic proves both rows blocked the network.
- Enforce blocks; report only produces a diagnostic.
Read the explained answer
Enforce blocks; report only produces a diagnostic. — Both rows have matched=false. blocked is true for enforce and false for report. A diagnostic does not prove the report was delivered.
The projected rich tree drops class and keeps em. Why is that not proof of a real sanitizer, and is a TrustedHTML label alone sanitization?
Read the explained answer
The allowlist declares p/em/strong and no attributes; the projected tree explains that decision. With projectorIsHTMLSanitizer=false, this is not verification of a complete HTML sanitizer. A TrustedHTML type does not itself prove sanitization quality. CSP does not sanitize HTML, and raw HTML may leave the autoescaping boundary. Later mutation may also fall outside the previously reviewed output.
□ I did not treat tree projection as an executed sanitizer.
□ I separated Trusted Types, CSP and sanitization roles.
Open the application console · request / response / browser
Also open request, URL, cookie and DOM examples
Sources and scope
- React — Common components: raw HTML boundarychildren props; Dangerously setting the inner HTML; TrustedHTML policy responsibility
- OWASP — XSS Prevention controlsFramework Security; Output Encoding; HTML Sanitization · Safe Sinks; Other Controls: CSP additional layer; post-sanitization mutation
- W3C Trusted Types — Working Draft, 7 October 2026§2.3 policies and creation responsibility; §2.4.1 CSP enforcement · §3.4–3.5 compliant input/string; §4.2.1 require-trusted-types-for; §4.2.2 trusted-types
- W3C CSP Level 3 — Working Draft, 16 September 2026§3.1 enforcing header; §3.2 report-only; §6.1.10 script-src
- WHATWG DOM — textContent§4.4 ordinary-element set text content creates Text node