Escaping Output

htmlspecialchars with ENT_QUOTES and UTF-8 turns user text into text the browser prints instead of runs.

Syntaxhtmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, ?string $encoding = null, bool $double_encode = true): string

Escaping means replacing the characters that mean something to a browser with harmless equivalents, at the moment you print them. < becomes &lt;, & becomes &amp;, a quote becomes &quot; or &#039;. The text still reads the same on the page. It no longer opens or closes a tag.

Without it, a visitor who types <script> into a comment box gets their code running in the browser of everyone who reads that comment. That is cross-site scripting, and escaping on output is the fix.

htmlspecialchars

Pass the flags and the encoding yourself: htmlspecialchars($value, ENT_QUOTES, 'UTF-8'). ENT_QUOTES converts both quote characters, which matters because escaped text often lands inside an attribute. Since PHP 8.1 the default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 and the default encoding follows default_charset, which is UTF-8. Before 8.1 the default left single quotes untouched, and code written then is still being copied, so being explicit removes the doubt.

Escape where you print

Store the raw value. Escape at every point of output. Escaping before storage loses the original, escapes twice on the second pass, and leaves you guessing later which values are already safe.

What trips people up

Attributes must be quoted in the markup. <p class=<?= e($x) ?>> is still exploitable, because an unquoted value ends at the first space.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Print a comment, a username or a profile bio that someone else typed.
  • Put a submitted value back into a form input after a failed validation.
  • Build a link whose query string contains a search term.
  • Pass a name into a script block on the page without breaking out of it.

More examples

One helper, used everywhere output happens

Typing the flags and the encoding at every echo invites the one place they get left out, so most plain-PHP projects define a short helper and never call htmlspecialchars() directly in a template. The author's apostrophe becomes &#039; because of ENT_QUOTES. Note the order in the second line: escaping happens first and nl2br() adds the <br> tags afterwards, so those tags survive. Here <?= e($x) ?> is shorthand for <?php echo e($x); ?> - the same statement, written short because templates are mostly markup.

Example · php
<?php

declare(strict_types=1);

function e(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}

$comment = ['author' => "O'Brien", 'body' => "Line one\nLine two"];
?>
<p>Posted by <?= e($comment['author']) ?></p>
<p><?= nl2br(e($comment['body'])) ?></p>

Escaping depends on where the value lands

htmlspecialchars() is an HTML escape, and HTML is not the only context on a page. A value going into a URL is percent-encoded first, because & and = mean something there. A value going into a script block is encoded as JSON, with the JSON_HEX_ flags so a closing tag inside the string cannot end the script element early. Using the wrong escape for the context is how escaped-looking code still ends up exploitable.

Example · php
<?php

declare(strict_types=1);

$term = 'php & "sql"';
$name = '</script><script>alert(1)</script>';

// In a URL: encode each value, then escape the finished URL for the attribute.
$url = '/search?' . http_build_query(['q' => $term]);
echo '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '">Results</a>';

// In JavaScript: htmlspecialchars is the wrong tool. Encode as JSON instead.
echo '<script>const user = '
    . json_encode($name, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR)
    . ';</script>';

Escaping is not the same as stripping

htmlspecialchars() keeps the text and disarms it. strip_tags() throws the tags away and keeps whatever was between them, which is why alert(1) survives in both stripped versions. Allowing <b> through means the output contains live markup again, so strip_tags() is a formatting tool, not a safety one. When a visitor genuinely needs to submit formatting, store their text as something that is not HTML - Markdown, for instance - and escape it on output as usual.

Example · php
<?php

declare(strict_types=1);

$input = 'Hello <b>there</b> <script>alert(1)</script>';

echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8'), "\n";
echo strip_tags($input), "\n";
echo strip_tags($input, '<b>'), "\n";

// Output:
// Hello &lt;b&gt;there&lt;/b&gt; &lt;script&gt;alert(1)&lt;/script&gt;
// Hello there alert(1)
// Hello <b>there</b> alert(1)

Discussion

  • Be the first to comment on this lesson.