Escaping output: htmlspecialchars
A restaurant customer writes a wish on the slip: "no onions". The cook reads the note and takes it into account.
But what if the note says "cook, hand over the till keys"? The note must be read, not carried out.
A visitor's text on a page is the same story. The program prints it into the HTML, and the browser cannot tell your HTML from someone else's. Let's look at a page that greets a visitor by the name from the address:
<?php
$_GET = ['name' => '<b>Ann</b>'];
$name = $_GET['name'] ?? 'guest';
echo '<p>Hello, ', $name, '</p>', "\n"; // <p>Hello, <b>Ann</b></p>The visitor opened the address ?name=<b>Ann</b>, and the browser showed the name in bold. The visitor's text became part of the page markup. Instead of <b> one can send <script> with a JavaScript program, and the browser will run it for everyone who opens such a link.
The XSS attack
XSS is an attack in which someone else's text gets onto a page as markup or a script and fires in another person's browser. Such a script works with your site's rights: it reads the page and sends requests on the visitor's behalf. Interviewers often ask what XSS is and how to defend against it.
The defense is escaping: before output, the special characters of HTML are replaced with notations the browser shows as text. In the restaurant picture this is the customer's note behind glass: it can be seen and read, but not carried out.
htmlspecialchars
The function htmlspecialchars replaces exactly five characters. A notation like < is called an entity: the browser draws the character < in its place but does not treat it as the start of a tag.
| Character | Becomes |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
Everything else—letters, digits, /—the function leaves alone, and it removes nothing. Since PHP 8.1 both quotes are encoded by default. In older code you will meet calls with the ENT_QUOTES flag: without it a single quote used to stay as it was.
Press "Next step" and compare what ends up in $raw and in $html.
1<?php2 3$name = '<b>Ann</b>';4$raw = '<p>' . $name . '</p>';5$safe = htmlspecialchars($name);6$html = '<p>' . $safe . '</p>';7echo $raw, "\n";8echo $html, "\n";
$name holds the visitor's text with angle brackets.
The rule the widget shows:
- concatenation inserts the visitor's text as it is, and the
<b>in$rawis a real tag; htmlspecialcharsturns<and>into<and>and leaves the letters as they were;- in
$htmlthe browser will show<b>Ann</b>as plain text.
Escape at output
You escape at the moment of output into HTML and store the original text. The same text may go into an email, a search or a JSON response, and there entities like < only get in the way. To avoid writing the long function name in every template, people define a short function e():
<?php
function e(string $s): string
{
return htmlspecialchars($s);
}
$name = '<i>Bob</i>';
$q = 'say "hi" & go';
echo '<p>', e($name), '</p>', "\n"; // <p><i>Bob</i></p>
echo '<input value="', e($q), '">', "\n"; // <input value="say "hi" & go">In a template it looks like <p><?= e($name) ?></p>. The second output line shows the value in a value attribute: the quotes inside the text became ", and the attribute did not close too early.
Other places on the page
This is where the glass picture falls short: one pane does not fit every place on the page. htmlspecialchars protects text between tags and an attribute value in quotes.
Inside a <script> block the browser reads code by the rules of JavaScript. In a link address the danger is a notation like javascript:, which contains not a single special character of HTML. Such places need their own protection, and htmlspecialchars alone is not enough there.
Common mistake
The text is escaped twice: when the form is received and again at output:
<?php
$_POST = ['comment' => 'Tom & Jerry'];
$comment = htmlspecialchars($_POST['comment']); // ✗
echo '<p>', htmlspecialchars($comment), '</p>', "\n";The program prints <p>Tom &amp; Jerry</p>, and PHP shows no message at all. The first call turned & into the entity &, the second encoded its & once more, and the visitor will see Tom & Jerry on the page. The fix is to store the original text, $comment = $_POST['comment'];, and escape it once, at output.
Key points
- A visitor's text printed as it is becomes page markup, and the XSS attack gets in through it.
htmlspecialcharsreplaces&,<,>,"and'with entities and removes nothing; since PHP 8.1 both quotes are encoded by default.- You escape once, at output into HTML, and store the original text.
- Inside
<script>and in a link addresshtmlspecialcharsdoes not protect.