Agent Wranglers

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

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.