Hacker DirectoryHacker Directory

WEB / WEB-10.S03

Escaping, sanitization, CSP and Trusted Types

Why are text escaping, formatting policy and typed-sink enforcement different controls?

CORE · RED · BLUE · ARTIFACT · PRACTICE

1. Core · Mechanism

Read every scenario without motion

Vetted rich text

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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

  1. 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
  2. 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
  3. 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
  4. 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

  1. 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
  2. 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
  3. 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
  4. 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

  1. 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
  2. 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
  3. 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
  4. 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. 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 &amp; <em>Bread</em></p>",
    "declaredRichOutput": "<p>Tea &amp; <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?

  1. It is not accepted; this row has expected=false.
  2. 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
  1. Any diagnostic proves both rows blocked the network.
  2. 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