Guide 014 worked guide

How to test whether a sanitizer invalidates an injection claim

Assess a suspected injection alert by tracing input transformation, sanitizer context, bypass routes, and the behavior needed to defeat the claim.

A sanitizer can defeat an injection claim only if it handles the attacker's actual input, is appropriate for the destination context, runs on every relevant path, and leaves no later step that reintroduces unsafe content. Prove those four facts for the exact code revision and deployed library/configuration. A call to a function named sanitize is not counterevidence by itself. If the sanitizer's version, options, or final rendering context is unknown, keep the claim under investigation.

For rich HTML, OWASP recommends HTML sanitization when users need to author markup and names DOMPurify as an option. OWASP also warns that modifying content after sanitization can void the protection. That makes the review a data-flow question: what bytes enter the sanitizer, what comes out, and where does the result go?

Illustrative case: a rich-text comment

Illustrative repository: Atlas Notes. Customers can write formatted comments. A scanner flags assignment to innerHTML and claims a stored cross-site scripting path. The simplified client code at revision r18 is:

const cleaned = DOMPurify.sanitize(comment.html, {
  USE_PROFILES: { html: true }
});
commentNode.innerHTML = cleaned;

The assignment is a sensitive rendering operation, and comment.html is untrusted. The sanitizer call between them may invalidate the specific claim, but first verify that cleaned is exactly what reaches innerHTML in the shipped bundle. The team also has a print-preview renderer that consumes comment.html separately; the client preview's sanitizer cannot defend that other sink.

The first review artifact is a small source-to-destination map:

Step Observed in example Question to resolve
Source Stored comment.html from a customer Could another field or imported comment bypass this path?
Transformation DOMPurify.sanitize with HTML profile Which DOMPurify version and configuration ship? Any custom hooks?
After transformation cleaned assigned directly to innerHTML Is there concatenation, templating, decoding, or library mutation in the actual bundle?
Destination HTML fragment in commentNode Is this always an HTML body context, or is the same value used in an attribute, URL, script, or preview?
Other consumer Print-preview renderer Does it sanitize independently for its own context?

A reviewer can now decide whether the alert is about the displayed comment in this client path, the print-preview path, or both. A safe conclusion for one path must not be copied to another.

Check the control in the right order

1. Name the injection claim precisely. Record the input source, attacker capability, destination operation, expected effect, code revision, and environment. “Unsafe user input” is too vague. Here the candidate says a customer can store markup that executes when another user opens the comment view.

2. Follow the exact value into and out of the sanitizer. The sanitized variable must derive from the input the scanner flagged, and the destination must receive that variable. Watch for fallback expressions such as using raw content when cleaning fails, and for transformations after cleaning. The CodeQL path-query guide is a useful model of the source-to-sink question, but the displayed path still needs source inspection.

3. Match the defense to the destination. HTML sanitization is meant for permitted HTML markup. It is not a general-purpose fix for SQL, shell arguments, JavaScript strings, CSS, or URL navigation. OWASP's XSS guidance separates output contexts; a value that is safe as an HTML fragment may need different handling elsewhere. If rich text is unnecessary, rendering as text through a safe text sink can be simpler than accepting HTML.

4. Inspect the shipped implementation. Identify the package version, configuration, hooks, DOM implementation if server-side, and build artifact. DOMPurify's own README warns that old server-side DOM implementations can create XSS risk even when DOMPurify's logic is correct. Library and browser behavior change, so a result observed with a current local package is not proof about an older production bundle.

5. Look for mutation after cleaning. Rendering libraries, markdown post-processors, string concatenation, entity decoding, and URL rewriting may change the sanitized fragment. OWASP specifically warns about post-sanitization modification. If such a step exists, inspect its output and threat model. “Sanitized earlier” is not a valid dismissal.

6. Validate the behavior in a controlled environment. With synthetic content and an authorized test account, check that permitted formatting remains and disallowed active constructs are removed or rendered inert in the actual destination. Observe the resulting DOM and the shipped package/configuration, not just the sanitizer's returned string. Use the sanitizer project's maintained cases and your application's own allowed-markup policy to choose representative inputs. A few passing examples challenge the claim but cannot prove every input safe.

Keep a small validation matrix instead of one “attack string passed” note:

Controlled input class Expected behavior Evidence to retain
Allowed paragraphs and links Permitted formatting remains under the published markup policy. Rendered DOM and policy version.
Disallowed active attributes or elements They are removed or inert at the final sink. Final DOM and sanitizer/bundle version.
Nested or encoded markup from maintained sanitizer cases It cannot acquire active behavior after parsing or another transform. Case identifier and final DOM, without customer content.
Same stored field in print preview Independently handled for that destination. Separate renderer and disposition.

Do not use DOMPurify's removed-element diagnostic as the security decision; the maintainers explicitly warn against that use. The final output and its context are what matter.

7. Review every consumer. Search for the stored field and any derived HTML across web views, email templates, exports, print previews, administrative screens, and mobile clients. The same input can be safe in one sink and dangerous in another. Record each consumer's separate control.

Filled decision for Atlas Notes

  • Candidate: Stored comment markup reaches innerHTML in the main comment view and executes as script.
  • Supporting evidence: The customer controls comment.html and the view uses innerHTML.
  • Counterevidence for the main view: At revision r18, the shipped bundle passes comment.html through a current DOMPurify HTML configuration and assigns the result directly to this HTML-fragment sink. The supplied source facts contain no post-sanitization transform on that path. Actual controlled validation and independent practitioner review remain necessary; no browser result is reported in this exercise.
  • Decision: Reject the claim for the main comment view at this revision and configuration, subject to independent review of the recorded source and bundle.
  • Remaining question: Print preview consumes the stored field through a different renderer. Its path is unresolved and needs its own review.

The italicized scope matters. A future plugin, config change, library version, or alternate sink can change the result. The controlled check is evidence about one build and selected cases, not a guarantee of zero XSS.

Follow the print-preview mutation

The main view's evidence concerns cleaned. Inspect whether print preview still uses that value at its final sink. In this constructed continuation, the preview adds a customer-supplied caption after cleaning the comment:

const cleaned = DOMPurify.sanitize(comment.html, {
  USE_PROFILES: { html: true }
});
preview.innerHTML = cleaned + "<p>" + comment.printCaption + "</p>";

No exact sanitizer output or browser execution is claimed here. The defect in the proposed counterevidence is visible without one: printCaption never enters the sanitizer. The allegation should be narrowed from “DOMPurify fails” to “the preview introduces a separately controlled HTML fragment after sanitization.”

Value Control applied Final use Supported conclusion
Formatted comment HTML sanitization Direct main-view HTML fragment Counterevidence can apply to this value and context, subject to version/configuration review.
Same cleaned comment Same sanitization First portion of preview HTML Still only accounts for the cleaned portion.
Plain-text print caption None in this implementation Concatenated into preview HTML Its markup is interpreted as HTML; sanitizer evidence does not cover it. Any claimed script execution still needs the exact destination/runtime conditions checked.

A benign caption <em>Quarterly note</em> makes the context error understandable: a feature specified as plain text would acquire formatting when parsed as HTML. It is a teaching input, not an XSS proof. The repair can preserve rich comments while making captions text:

preview.innerHTML = cleaned;
const caption = document.createElement("p");
caption.textContent = comment.printCaption;
preview.append(caption);

The constant element name and ordinary paragraph text sink matter. The code is illustrative and has not been executed. In controlled validation, permitted comment paragraphs and links should remain; the caption should display its literal text; project-maintained sanitizer cases should be checked in the actual bundle and DOM environment. Inspect final DOM behavior, not only a string or a removed-element diagnostic. OWASP's XSS guidance and DOMPurify's maintainers warn that later mutation can undermine sanitization.

Decision: retain the main-view rejection only within its reviewed sanitizer/configuration scope; open the narrower preview-caption claim for independent verification. Do not suppress every innerHTML alert because one value was cleaned.

Review changes to the allowed markup as product decisions

The sanitizer configuration expresses what the product intends to allow. A developer may add an element, attribute or custom hook to support a new formatting feature without touching the final innerHTML assignment. That change can invalidate an earlier review because the set of permitted outputs has changed. Compare the actual configuration and hooks when carrying a rejection forward, rather than checking only whether the sanitizer call remains present.

In Atlas, the comment and caption have different product rules. Comments intentionally permit limited formatting; captions are plain text. Write those rules next to the two rendering paths. A request to preserve caption markup is a new feature decision that would require a new output policy and review, not a reason to remove textContent casually. Likewise, a text-only replacement for rich comments may prevent HTML interpretation while also breaking the intended feature. Security review should make that tradeoff explicit.

The acceptance notes should therefore include both security and functional expectations. For comments, identify the allowed formatting that must survive and the final DOM context in which it is used. For captions, identify literal text as the intended output. For print preview, ensure layout assembly does not reinterpret the caption after it was inserted as text. This keeps the repair focused on the uncontrolled value rather than disabling the whole preview.

When a package or DOM environment changes, reuse only the evidence that still applies. The source-to-sink map may be unchanged, while library-specific assumptions need refreshing against the deployed version. A successful old browser check remains a historical observation, not a guarantee about the new bundle. Conversely, a source refactor that does not change the sanitizer, context or value flow may need a narrower review than a complete reinvestigation.

The final decision should name the property that defeated the claim: the particular untrusted value receives an appropriate control, its result reaches the intended context, and no later step adds uncontrolled HTML. “Uses DOMPurify” is too coarse to preserve those relationships across future changes.

Check the value after every transformation that follows sanitization. A print-layout helper may combine the sanitized comment with an unsanitized caption; the safe comment does not confer protection on its neighbor. Draw those values as separate inputs in the review notes so a repair to one cannot accidentally close the other's claim.

A useful maintenance comment belongs near the assembly boundary: identify which value is approved limited markup and which must remain literal text. Avoid an unsupported declaration that all content reaching the function is safe. If the interface cannot express that distinction, the owner can consider a clearer contract during the repair, while retaining the immediate source-to-sink checks. The purpose is to preserve the reviewed property when the next formatting feature arrives, not merely to keep one scanner result quiet.

For SQL, the relevant artifact is the query/driver boundary rather than this HTML analysis: use 003's fixed-identifier exercise and 009's bound-value dispute.