PHP preventing XSS with htmlspecialchars
Encode user text before printing it into HTML, and escape for the right context.
<?php
function e(string $text): string
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");
}
$comment = '<script>alert("x")</script>';
echo "<p>" . $comment . "</p>", "\n";
echo "<p>" . e($comment) . "</p>", "\n";
$name = "O'Brien & \"Sons\"";
echo '<input value="' . e($name) . '">', "\n";
echo "q=" . urlencode("a b&c=d"), "\n";
echo json_encode(["note" => "</script>"], JSON_HEX_TAG), "\n";<p><script>alert("x")</script></p>
<p><script>alert("x")</script></p>
<input value="O'Brien & "Sons"">
q=a+b%26c%3Dd
{"note":"\u003C\/script\u003E"}Explanation
Cross-site scripting (XSS) happens when user-controlled text is put into a page as HTML. If a visitor submits a comment containing a script tag, and the page prints it as it is, the script runs in other visitors' browsers. The defence is to encode output for the place where it is inserted. For HTML text and attribute values, htmlspecialchars converts &, <, >, and quotes into entities so the browser shows them as text.
Use the flags ENT_QUOTES | ENT_SUBSTITUTE and say which character set is in use, normally UTF-8, so that both quote types are encoded and invalid bytes do not slip through. Encode at the moment of output, not when saving, because the right encoding depends on the destination. Other contexts need other functions: urlencode for query strings and json_encode for data placed in scripts. Template engines such as Twig and Blade escape by default.
Explore the dry run
<?phpfunction e(string $text): string{ return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, "UTF-8");} $comment = '<script>alert("x")</script>';echo "<p>" . $comment . "</p>", "\n";echo "<p>" . e($comment) . "</p>", "\n"; $name = "O'Brien & \"Sons\"";echo '<input value="' . e($name) . '">', "\n";echo "q=" . urlencode("a b&c=d"), "\n";echo json_encode(["note" => "</script>"], JSON_HEX_TAG), "\n";Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Encode &, <, >, and both kinds of quotes, in UTF-8. | — | — | |
| Unescaped, the browser would treat this as a script element and run it. | — | <p><script>alert("x")</script></p> | |
| Escaped, the same text is shown as characters and nothing runs. | — | <p><script>alert("x")</script></p>
<p><script>alert("x")</script></p> | |
| Inside an attribute, the quotes must be encoded too, or the value could break out of the attribute. | — | <p><script>alert("x")</script></p>
<p><script>alert("x")</script></p>
<input value="O'Brien & "Sons""> | |
| A query string needs URL encoding, not HTML escaping. | — | <p><script>alert("x")</script></p>
<p><script>alert("x")</script></p>
<input value="O'Brien & "Sons"">
q=a+b%26c%3Dd | |
| Data embedded in a script must be JSON-encoded, with JSON_HEX_TAG so that </script> cannot close the script element. | — | <p><script>alert("x")</script></p>
<p><script>alert("x")</script></p>
<input value="O'Brien & "Sons"">
q=a+b%26c%3Dd
{"note":"\u003C\/script\u003E"} |
Cost and common mistakes
O(n) for a string of n characters.
- Without ENT_QUOTES, single quotes are not encoded in older PHP versions, so an attribute in single quotes can still be broken out of.
- Escaping twice shows ugly text such as &lt;. Escape once, at output.
- Escaping does not make a string safe for every context. A value used in a URL, a script, a style, or an SQL statement needs the encoding of that context, or better, a parameterized API.
Try it yourself
Why is the single quote encoded as '?
Reveal answer and reasoning
So that a value placed inside a single-quoted HTML attribute cannot end the attribute early and add new attributes such as onclick.