Escaping Output (htmlspecialchars)
Why Output Must Be Escaped In simple terms: When you print user-supplied text into a web page, the browser cannot tell your HTML apart from the user's text. If a user's "name" contains HTML or JavaScr
Why Output Must Be Escaped
In simple terms: When you print user-supplied text into a web page, the browser cannot tell your HTML apart from the user's text. If a user's "name" contains HTML or JavaScript, the browser will run it. Escaping converts special characters into safe codes, so the browser displays them instead of executing them.
Technically: Characters such as <, >, &, ", and ' have special meaning in HTML. htmlspecialchars() converts them into HTML entities (<, >, &, ", '). The browser renders entities as the original characters, but never interprets them as markup.
The Attack: Cross-Site Scripting (XSS)
<?php
$name = '<script>alert("XSS")</script>'; // submitted by an attacker
?>
<p>Unsafe: <?= $name ?></p>
<p>Safe: <?= htmlspecialchars($name) ?></p>
Output (HTML)
<p>Unsafe: <script>alert("XSS")</script></p>
<p>Safe: <script>alert("XSS")</script></p>
In the first line, the browser executes the script. In the second, the visitor simply sees the text <script>alert("XSS")</script>. A real attacker would not show an alert; the script could steal session cookies, change the page content, submit forms as the victim, or redirect to a phishing site. XSS is one of the most common web vulnerabilities, and consistent output escaping is the main defense.
htmlspecialchars()
Syntax
htmlspecialchars(
string $string,
int $flags = ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401,
?string $encoding = null, // defaults to UTF-8
bool $double_encode = true
): string
Characters Converted
| Character | Entity |
|---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
Example
<?php
echo htmlspecialchars("O'Reilly & Sons <books>") . "\n";
Output
O'Reilly & Sons <books>
Default Flags (PHP 8.1+)
Since PHP 8.1, the default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401:
ENT_QUOTESescapes both double and single quotes. Before PHP 8.1, single quotes were not escaped by default, which made single-quoted attributes vulnerable. In code that must run on older versions, passENT_QUOTESexplicitly.ENT_SUBSTITUTEreplaces invalid UTF-8 sequences with the replacement character�instead of returning an empty string.
Invalid UTF-8
<?php
var_dump(htmlspecialchars("abc\xFF"));
var_dump(htmlspecialchars("abc\xFF", ENT_QUOTES));
Output
string(6) "abc�"
string(0) ""
Without ENT_SUBSTITUTE, invalid input returns an empty string, so content silently disappears. The default flags in PHP 8.1+ avoid this. (The replacement character takes 3 bytes in UTF-8, so the result is 6 bytes long.)
Double Encoding
<?php
echo htmlspecialchars('Fish & Chips') . "\n";
echo htmlspecialchars('Fish & Chips', double_encode: false) . "\n";
Output
Fish & Chips
Fish & Chips
By default, existing entities are escaped again, which displays the literal text & in the browser. Double encoding usually means the data was already escaped before it was stored — fix that at the source (store raw text, escape only on output) rather than using double_encode: false everywhere.
Escape for the Context
HTML is not the only context. Each place where data is inserted needs its own escaping:
| Context | Example | Escaping |
|---|---|---|
| HTML text | <p><?= ... ?></p> | htmlspecialchars() |
| HTML attribute (quoted) | <input value="<?= ... ?>"> | htmlspecialchars(), attribute must be quoted |
| URL query parameter | href="/search?q=<?= ... ?>" | urlencode() (or http_build_query()), then htmlspecialchars() |
Whole URL in href/src | <a href="<?= ... ?>"> | Validate the scheme (http/https), then htmlspecialchars() |
| JavaScript data | <script>const x = <?= ... ?>;</script> | json_encode() with the JSON_HEX_* flags |
| CSS | style="..." | Avoid inserting user data into CSS |
| SQL | Queries | Prepared statements (not escaping functions) |
Attributes Must Be Quoted
<?php
$value = 'x onfocus=alert(1) autofocus';
?>
<input value=<?= htmlspecialchars($value) ?>>
<input value="<?= htmlspecialchars($value) ?>">
Output (HTML)
<input value=x onfocus=alert(1) autofocus>
<input value="x onfocus=alert(1) autofocus">
htmlspecialchars() does not escape spaces or =. In the unquoted attribute, the space ends the value, and the rest becomes new attributes — an onfocus handler that runs JavaScript. In the quoted attribute, the whole string stays inside the value. Always quote attributes.
URLs
<?php
$search = 'a&b <c>';
?>
<a href="/search?q=<?= htmlspecialchars(urlencode($search)) ?>">Search again</a>
Output (HTML)
<a href="/search?q=a%26b+%3Cc%3E">Search again</a>
urlencode() makes the value safe inside the URL (& would otherwise start a new parameter); htmlspecialchars() then makes the URL safe inside the HTML attribute. For URLs supplied entirely by users, also check the scheme — see PHP Form URL/E-mail.
Passing Data to JavaScript
<?php
$userName = '</script><script>alert(1)</script>';
$json = json_encode($userName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
?>
<script>
const userName = <?= $json ?>;
</script>
Output (HTML)
<script>
const userName = "\u003C\/script\u003E\u003Cscript\u003Ealert(1)\u003C\/script\u003E";
</script>
json_encode()produces a valid JavaScript string literal, including the quotes.JSON_HEX_TAGencodes<and>as\u003Cand\u003E, so the value cannot close the<script>element early. The otherJSON_HEX_*flags encode&,', and".- Do not use
htmlspecialchars()inside<script>; HTML entities are not decoded there and would appear literally.
A safer alternative is to put data in a data- attribute (escaped with htmlspecialchars()) and read it in JavaScript with element.dataset.
Line Breaks: nl2br() After Escaping
<?php
$message = "Line 1\n<b>Line 2</b>";
echo nl2br(htmlspecialchars($message)) . "\n";
Output
Line 1<br />
<b>Line 2</b>
Escape first, then convert newlines to <br />. In the reverse order, htmlspecialchars() would escape the <br /> tags themselves.
A Short Helper: e()
Escaping must happen on every output, so a short helper reduces noise in templates:
<?php
function e(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
Flags and encoding are passed explicitly, so the behavior is identical on every PHP version and configuration. Accepting null avoids deprecation notices when optional values are missing.
Practical Example: Displaying Comments Safely
<?php
function e(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$comments = [
['author' => 'Asha', 'text' => "Great lesson!\nThanks."],
['author' => '<img src=x onerror=alert(1)>', 'text' => 'Nice <b>tips</b>'],
];
?>
<?php foreach ($comments as $comment): ?>
<div class="comment">
<strong><?= e($comment['author']) ?></strong>
<p><?= nl2br(e($comment['text'])) ?></p>
</div>
<?php endforeach; ?>
Output (HTML)
<div class="comment">
<strong>Asha</strong>
<p>Great lesson!<br />
Thanks.</p>
</div>
<div class="comment">
<strong><img src=x onerror=alert(1)></strong>
<p>Nice <b>tips</b></p>
</div>
What Happens
- The first comment is displayed normally, with its line break converted to
<br />. - The second comment's "author" is an attack: an image tag whose
onerrorhandler would run JavaScript. After escaping, it appears as harmless text. - HTML formatting in comments (
<b>) is shown literally rather than rendered. If your application should allow formatting, use a safe markup format (such as Markdown rendered by a library in safe mode) or an HTML sanitizer — never print raw HTML from users.
Escape on Output, Not on Input
| Approach | Problem |
|---|---|
| Escape when saving to the database | Data becomes HTML-specific; it shows & in emails, CSV exports, APIs, and gets double-encoded in HTML |
| Escape when outputting | Data stays raw and reusable; each output applies the correct escaping |
Store the user's text exactly as validated, and escape it at the moment it is placed into HTML, a URL, JavaScript, or another context.
Template Engines Escape Automatically
Twig ({{ name }}) and Laravel Blade ({{ $name }}) escape output by default, which makes forgetting it much less likely. Their "raw" syntax ({{ name|raw }}, {!! $name !!}) disables escaping and should be used only for trusted HTML.
Common Mistakes
Forgetting to Escape "Safe-Looking" Data
Effect: XSS through names, titles, search terms, error messages, or data from APIs.
Fix: Escape every dynamic value, regardless of its source.
Unquoted Attributes
Effect: Attribute injection despite escaping.
Fix: Always use quotes around attribute values.
Escaping With the Wrong Function
Effect: htmlspecialchars() inside <script>, or addslashes() for HTML, leaves the context vulnerable.
Fix: Use the escaping method for the specific context.
Escaping Before Storing
Effect: Double-encoded text and broken non-HTML outputs.
Fix: Store raw data; escape on output.
Relying on strip_tags()
Effect: Remaining attributes and text can still be dangerous.
Fix: Escape with htmlspecialchars(), or use an HTML sanitizer for rich text.
Best Practices
- Escape every dynamic value at output time with
htmlspecialchars()(or ane()helper). - Quote all HTML attributes.
- Use
urlencode()/http_build_query()for URL parameters andjson_encode()withJSON_HEX_*flags for JavaScript. - Validate URL schemes for user-supplied links.
- Prefer template engines with automatic escaping, and use raw output only for trusted content.
Official reference: https://www.php.net/manual/en/function.htmlspecialchars.php