APS-3Required
Escape everything you show
Escape untrusted text for the place it lands in a page, and back it up with a strict Content Security Policy.
How to do it
Use a template system that escapes by default, such as Jinja2, Twig, Razor or React. Avoid its raw output switch, such as |safe or dangerouslySetInnerHTML. In the browser, set textContent, not innerHTML. Clean rich text with a sanitiser such as DOMPurify. Allow only http: and https: in links that users supply, because escaping does not stop a javascript: link. Block inline scripts with a Content Security Policy. The website rules allow only the scripts you wrote and lock down risky DOM writes cover the browser side.
Examples
JavaScript example
// Bad
el.innerHTML = review.text;
// Good
el.textContent = review.text;
Python example
# Python: escape before building HTML by hand (Jinja2 and Django templates do this for you)
from markupsafe import escape
html = f"<p>{escape(review_text)}</p>"
PHP example
// PHP
echo '<p>' . htmlspecialchars($review, ENT_QUOTES, 'UTF-8') . '</p>';
How to check
Put <b>PENTEST</b> and "><svg onload=alert('PENTEST')> in every field that shows up again, such as names, reviews and search terms. Put javascript:alert('PENTEST') in every link field, such as a website address. The page shows the text as typed, runs nothing and refuses the link. The page's Content-Security-Policy header has a nonce or hashes in script-src, plus 'strict-dynamic', object-src 'none' and base-uri 'none' or 'self'. A list of allowed hosts alone fails, because such lists are easy to bypass.
Sources
Related rules
- STK-27 Use DOMPurify or nh3 to clean user HTML in Software choices
- CSS-35 Keep untrusted data out of stylesheets in HTML and CSS
- CSS-37 Never read elements through window globals in HTML and CSS
- CSS-24 Clone markup that scripts add from a template element in HTML and CSS
- DSK-35 Insert text into web pages as text from content scripts in Desktop apps and extensions
- LOC-47 Escape translated text like user input in Localization
- SRH-1 Search with the database's own full-text index in Search
- APS-25 Allow only the scripts you wrote in Application security
- APS-35 Lock down risky DOM writes in Application security
- APS-36 Sending the old XSS filter header in Application security
For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/input-and-injection/cross-site-scripting">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.