ASVS 5.0 V1.2.1 (Level 1) requires that anything you write into an HTTP response, HTML document or XML document is encoded for the exact context it lands in: an HTML element, an HTML attribute, an HTML comment, CSS, or an HTTP header field. The goal is that untrusted data can never change the structure of the message or page.
Why it matters
Cross site scripting remains one of the most common web flaws (CWE-79), and almost every instance comes down to one mistake: data was placed in a context with the wrong encoding, or none. A value that is harmless in a text node can break out of an unquoted attribute. A value that is safe in an attribute can still inject a new header if it contains a line break and lands in a response header.
Contexts have different special characters:
- HTML body:
<,>,& - Quoted attribute: all of the above plus the quote characters
- Unquoted attribute: whitespace and many more, which is why you should always quote
- CSS values: backslash, quotes, parentheses, semicolons
- HTTP headers: carriage return and line feed
- HTML comments: the closing sequence of a comment
One encoder cannot cover all of these correctly.
How to implement it
Let the framework do it, and make the safe path the default. React, Vue, Angular, Jinja2 (autoescape on), Razor, Go html/template and Rails ERB all encode for the HTML body and quoted attributes automatically. Your job is to avoid the escape hatches: dangerouslySetInnerHTML, v-html, |safe, Html.Raw, raw.
Where you must build output yourself, use a maintained encoder for the specific context. In Java, the OWASP Java Encoder gives you one method per context:
import org.owasp.encoder.Encode;
String safeBody = Encode.forHtmlContent(userName);
String safeAttr = Encode.forHtmlAttribute(userTitle);
String html = "<a title=\"" + safeAttr + "\">" + safeBody + "</a>";
For headers, reject line breaks outright rather than trying to encode them. Most modern servers already refuse them, but custom code that sets raw headers needs a check:
def set_safe_header(response, name: str, value: str):
if "\r" in value or "\n" in value:
raise ValueError("line break in header value")
response.headers[name] = value
For CSS, do not place untrusted data into style blocks at all. Map user choices to a fixed set of classes or CSS variables whose values you control.
const ALLOWED_THEMES = new Set(['light', 'dark', 'contrast']);
const theme = ALLOWED_THEMES.has(req.query.theme) ? req.query.theme : 'light';
Always put attribute values in double quotes in templates you write by hand, since encoders for quoted attributes assume quotes.
How to test it
Send a polyglot string through every field that is echoed back. A simple set to try:
"><svg onload=alert(1)>for attribute breakout</title><script>alert(1)</script>for body and title context--><img src=x onerror=alert(1)>for comment contextx%0d%0aSet-Cookie: a=bfor header injection
Then read the response source, not the rendered page. In each context the dangerous characters must appear as entities or be rejected. A passing result for the first string looks like "><svg onload=alert(1)> inside the attribute.
Automated dynamic scanning is good at this because it replays payloads in context. Try the interactive demo below to see a scan flag unencoded output.
Click through it at your own pace, or open it full screen.
Also grep your code for the escape hatches listed above and review each one by hand.
Common mistakes
- Using HTML encoding for data placed inside a JavaScript block or an
onclickhandler. That is a different context and needs JavaScript encoding or, better, a data attribute read by script. - Leaving attribute values unquoted.
- Encoding at input time and then reusing the value in another context.
- Trusting a sanitizer to fix a context problem. Sanitizers clean HTML. They do not make data safe in CSS or headers.
- Building a Content-Disposition filename from user input without filtering quotes and line breaks.
Try it yourself
Take one page of your application that echoes a value in several places, for instance a search results page that shows the query in the title, a heading, a hidden input and a link. View the source and write down each context. Then run these checks:
- Put
"onmouseover="alert(1)in the query and confirm the attribute value is encoded and the quote does not close the attribute. - Put
</title><b>xin the query and check that the title element still ends where you expect. - Put
javascript:alert(1)in a field that becomes a link and confirm it does not become a workinghref.
If you find a hit, fix the template and add a regression test that renders the template with the payload and asserts on the output string. Pair this with a check in your pull request pipeline that fails when a raw output helper appears in a diff, so every use gets a human look.
Quick check
Why is a single escape function not enough?
Each context has its own special characters. A function that handles angle brackets does nothing for quote breakout in an attribute or line breaks in a header.
What is the safest default for HTML output?
A framework with automatic escaping, and a rule against using its raw output escape hatches on untrusted data.