Output escaping is necessary but not sufficient for safe rendering. This page collects the things the per-context guides only hint at.
It is tempting to escape data once, at the boundary where it enters the system, and store the escaped form in the database. Do not do this:
- The "escape" is context-dependent — HTML-escaping a value at input time makes it wrong for JSON, CSV, JavaScript, plain-text email, search indexing, …
- Once stored as escaped text, you lose the original value and have to un-escape it for every non-HTML use, which inevitably leaks an unescaped path.
Store the original bytes. Escape them at the place — and in the context — they are about to be rendered.
htmlspecialchars() is not enough on its own. Each of the five
contexts has different rules, and applying the wrong one can leave the
output exploitable:
| Output location | Wrong escaper | What attacker can do |
|---|---|---|
| Unquoted attribute | escHtml |
inject onmouseover=… via a space |
<script> body |
escHtml |
HTML entities are not decoded in scripts; payload survives intact |
<style> body |
escHtml |
same problem as <script> |
href/src |
escHtml only |
javascript: scheme runs code |
| Inline event handler | escHtmlAttr only |
the attribute is safe but its JS contents are not |
The matrix in getting started shows the right pairing for each location.
The URL escaper does not (and cannot) prevent javascript: or
data: URLs from running. If the URL itself comes from user input,
validate the scheme against a whitelist before escaping:
$url = $userSuppliedUrl;
$scheme = strtolower(parse_url($url, PHP_URL_SCHEME) ?? '');
if (!in_array($scheme, ['http', 'https', 'mailto'], true)) {
$url = '#';
}
echo '<a href="' . Esc::esc($url, 'attr') . '">link</a>';Even with perfect escaping, a CSP header narrows the blast radius of
any escaping bug you may still have. A minimal, strict policy is a
strong second line of defence; a permissive unsafe-inline policy
undoes a lot of what escaping bought you. Treat them together, not as
substitutes.
The escapers protect values embedded in a fixed code template. They do not protect against building executable code from user input — do not concatenate untrusted text into a JavaScript identifier, a CSS selector, a SQL fragment, a shell command, or a regular expression. Use placeholders / prepared APIs in each of those contexts.
When in doubt about a specific edge case, defer to:
- OWASP XSS Prevention Cheat Sheet
- WHATWG HTML — § Restrictions on content models
- RFC 3986 — Uniform Resource Identifier
- CSS Syntax Module Level 3 — § Escapes
If you discover an escaping bug in this package, please report it through the process in SECURITY.md rather than opening a public issue.