Skip to content
Theory · PHP and the web · lesson 4 of 7

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
<?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 &lt; is called an entity: the browser draws the character < in its place but does not treat it as the start of a tag.

CharacterBecomes
&&amp;
<&lt;
>&gt;
"&quot;
'&#039;

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.

LiveA visitor's text before and after htmlspecialcharsstep 1 of 7 · frames: 1

$name holds the visitor's text with angle brackets.

Compare $raw and $html

The rule the widget shows:

  • concatenation inserts the visitor's text as it is, and the <b> in $raw is a real tag;
  • htmlspecialchars turns < and > into &lt; and &gt; and leaves the letters as they were;
  • in $html the 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 &lt; only get in the way. To avoid writing the long function name in every template, people define a short function e():

PHP
<?php

function e(string $s): string
{
    return htmlspecialchars($s);
}

$name = '<i>Bob</i>';
$q = 'say "hi" & go';
echo '<p>', e($name), '</p>', "\n"; // <p>&lt;i&gt;Bob&lt;/i&gt;</p>
echo '<input value="', e($q), '">', "\n"; // <input value="say &quot;hi&quot; &amp; 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 &quot;, 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
<?php

$_POST = ['comment' => 'Tom & Jerry'];
$comment = htmlspecialchars($_POST['comment']); // ✗
echo '<p>', htmlspecialchars($comment), '</p>', "\n";

The program prints <p>Tom &amp;amp; Jerry</p>, and PHP shows no message at all. The first call turned & into the entity &amp;, the second encoded its & once more, and the visitor will see Tom &amp; 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.
  • htmlspecialchars replaces &, <, >, " 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 address htmlspecialchars does not protect.