ASVS 5.0 V1.1.2 (Level 2) requires that output encoding and escaping happen as the final step before data reaches the interpreter it is meant for, or that the interpreter does it itself, as parameterized queries and auto escaping templates do. Encoding early and storing the result is the pattern it rules out.
Why it matters
Encoding is only correct for one destination. HTML encoding protects an HTML body but does nothing for a SQL statement, a shell command or a JavaScript string. If you encode at input time, you have to guess every place that value will ever go, and you will guess wrong. Worse, the stored value becomes corrupted: a name like O'Brien saved as O'Brien shows up garbled in emails, CSV exports and APIs.
When encoding sits right next to the sink, a reviewer can see the context and verify that the right encoder is in use.
How to implement it
Keep stored data raw. At each sink, either use a mechanism where the interpreter handles separation, or apply the context specific encoder in the line before the sink.
Interpreter handled options, which are the best choice:
- Parameterized queries for SQL.
- Auto escaping template engines such as Jinja2 with autoescape on, React JSX, or Go html/template.
- Argument arrays for process execution instead of shell strings.
When you build output by hand, encode at the last moment:
from markupsafe import escape
def render_greeting(name: str) -> str:
# name stays raw everywhere else in the system
return "<p>Hello, " + str(escape(name)) + "</p>"
Avoid helper functions named sanitize_input that rewrite data on the way in. Rename the thinking: input gets validated, output gets encoded.
How to test it
Store a value containing characters that matter to several interpreters, such as "><img src=x onerror=alert(1)>'; --. Then trace it to every sink: HTML page, JSON API, email body, CSV export, log line, SQL. In each one it must display as literal text and must not change the structure of the output. Also check that the database holds the original value, not an encoded one.
Common mistakes
- Encoding on input and then trusting the data everywhere.
- Encoding twice, which shows
&lt;to users. - Using one generic escape function for every context.
- Concatenating after encoding, so that unencoded text slips into the final string.
Where this shows up in real projects
The usual breakage is an input sanitizer that someone added years ago. It strips or escapes angle brackets at the API, so the database holds <b>. Later a mobile app, a PDF export and a search index each consume that data, and each shows the escaped text or, worse, decodes it again in the wrong place. Removing the input side rewriting and moving encoding to each sink fixes all of them at once, and it usually deletes code.
A useful review question is "who is the interpreter here?" For an HTML template, it is the browser. For a query, it is the database. For a log line, it is the log viewer. Name it, then confirm the encoder in the line above the sink matches that interpreter. If the answer is "the framework handles it", confirm the framework is configured for it and that no raw output escape hatch is used on this value.
Also watch for pipelines where one service's output is another service's input. A value encoded for HTML in service A and consumed by service B as plain text is a bug in A, since A could not know its destination.
Quick check
Where should output encoding happen?
Immediately before the data reaches the interpreter, or inside the interpreter through parameterization or auto escaping.