Escaping Output
htmlspecialchars with ENT_QUOTES and UTF-8 turns user text into text the browser prints instead of runs.
htmlspecialchars(string $string, int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, ?string $encoding = null, bool $double_encode = true): stringEscaping means replacing the characters that mean something to a browser with harmless equivalents, at the moment you print them. < becomes <, & becomes &, a quote becomes " or '. 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
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 ' 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.
<?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.
<?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.
<?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 <b>there</b> <script>alert(1)</script>
// Hello there alert(1)
// Hello <b>there</b> alert(1)
Discussion