Strings7 min readExamples run on PHP 8.5.10

htmlspecialchars()

Learn how PHP 8.4 htmlspecialchars() converts &, <, > and quotes to HTML entities, how its flags and double_encode work, and which mistakes to avoid.

Signature

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

Purpose

htmlspecialchars() turns characters that have a special meaning in HTML into entities, so the browser shows them as text instead of parsing them as markup. It converts only a small set: the ampersand, double quote, single quote (depending on flags), less-than and greater-than. When you print user-supplied text into an HTML page, this is the usual first line of defence against broken markup and cross-site scripting.

Signature (PHP 8.4)

Parameters and defaults

  • $string: the text to convert.
  • $flags: a bitmask that controls three things: quote handling (ENT_COMPAT, ENT_QUOTES, ENT_NOQUOTES), handling of invalid byte sequences (ENT_SUBSTITUTE, ENT_IGNORE) and the document type (ENT_HTML401, ENT_HTML5, ENT_XHTML, ENT_XML1). Since PHP 8.1.0 the default is ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401.
  • $encoding: the character set of the input. When it is null, PHP uses the default_charset setting. For UTF-8 pages, passing 'UTF-8' explicitly is a good habit.
  • $double_encode: when true (the default), everything is converted, including the & of an existing entity such as &amp;. When false, existing entities are left alone.

Return value and failure cases

The function returns the converted string. If the input contains an invalid code unit sequence for the chosen encoding, it returns an empty string, unless ENT_SUBSTITUTE (replace with U+FFFD) or ENT_IGNORE (silently drop) is set. The default flags include ENT_SUBSTITUTE, so invalid UTF-8 normally becomes a replacement character rather than an empty string. An unrecognised encoding name makes PHP use the default encoding and emit a warning.

Example 1: the default conversion

With the default flags, both kinds of quotes are converted. A single quote becomes &#039; in HTML 4.01 mode.

Example
<?php
$input = "<a href='test'>Tom & \"Jerry\"</a>";
echo htmlspecialchars($input), "\n";
Output
&lt;a href=&#039;test&#039;&gt;Tom &amp; &quot;Jerry&quot;&lt;/a&gt;

Example 2: how flags change quotes

Quote flags matter most when the output goes inside an HTML attribute. ENT_COMPAT leaves single quotes alone, ENT_NOQUOTES leaves both alone, and ENT_HTML5 writes a single quote as &apos;.

Example
<?php
$text = "She said \"hi\" & it's fine";
echo json_encode([
    'default' => htmlspecialchars($text),
    'compat' => htmlspecialchars($text, ENT_COMPAT),
    'noquotes' => htmlspecialchars($text, ENT_NOQUOTES),
    'html5' => htmlspecialchars($text, ENT_QUOTES | ENT_HTML5),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"default":"She said &quot;hi&quot; &amp; it&#039;s fine","compat":"She said &quot;hi&quot; &amp; it's fine","noquotes":"She said \"hi\" &amp; it's fine","html5":"She said &quot;hi&quot; &amp; it&apos;s fine"}

Example 3: double_encode and invalid UTF-8

The first two lines show the effect of double_encode. The last two lines use bin2hex() to show the bytes returned for an invalid UTF-8 byte (\x80), first without and then with ENT_SUBSTITUTE. Without it, the result is an empty string, so the third line is blank.

Example
<?php
$text = "Fish &amp; Chips < 5";
echo htmlspecialchars($text), "\n";
echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false), "\n";
$bad = "atta\x80ck";
echo bin2hex(htmlspecialchars($bad, ENT_QUOTES, 'UTF-8')), "\n";
echo bin2hex(htmlspecialchars($bad, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')), "\n";
Output
Fish &amp;amp; Chips &lt; 5
Fish &amp; Chips &lt; 5

61747461efbfbd636b

Edge cases

  • If neither ENT_COMPAT, ENT_QUOTES nor ENT_NOQUOTES is present in $flags, the behaviour is ENT_NOQUOTES. If several are present, ENT_QUOTES wins, then ENT_COMPAT. Passing a custom flags value that omits a quote flag therefore stops converting quotes.
  • If several document-type flags are present, ENT_HTML5 has the highest precedence. If none is present, ENT_HTML401 applies.
  • ENT_IGNORE is discouraged because silently dropping bytes may have security implications. Prefer ENT_SUBSTITUTE.
  • The function does not translate other characters, such as accented letters or symbols. If you need every character that has a named entity converted, use htmlentities().
  • UTF-8, ISO-8859-1, ISO-8859-15, cp1251, cp1252, cp866 and KOI8-R are effectively equivalent for this function, because the characters it changes sit at the same positions, as long as the string is valid in that encoding.

Common mistakes

  1. Escaping on input instead of on output. Store the original text and escape it when you write it into HTML. Escaping early leads to double-escaped data and wrong results in other contexts.
  2. Using it for every context. The function prepares text for HTML content and quoted attributes. It does not make a value safe inside JavaScript, CSS or a URL. Those contexts need their own encoding.
  3. Dropping the quote flags. If you pass ENT_NOQUOTES or a flags value without a quote flag, an attacker may close an attribute value with a quote.
  4. Double-escaping. Running the function twice turns &lt; into &amp;lt;. Escape once, at the last moment. Use double_encode: false only when the input is known to contain valid entities that you want to keep.
  5. Mismatched encoding. Declare the real character set of the page and the input. Relying on a wrong default_charset can give empty or garbled results.
  6. Expecting it to remove tags. It makes tags visible as text. It does not delete them. strip_tags() is a different tool with different trade-offs.
  • htmlspecialchars_decode() converts the special entities back to characters.
  • htmlentities() converts all characters that have named entities.
  • get_html_translation_table() shows the translation table in use.
  • strip_tags() removes HTML and PHP tags.
  • nl2br() inserts <br> elements before newlines, and is usually applied after escaping.

Recap

htmlspecialchars() converts &, <, > and (by default) both quote types into entities. The 8.4 defaults are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401. Always escape at output time, keep the encoding correct, leave double_encode at true unless you have a reason, and remember that it only covers HTML text and attribute contexts.

Tested examples

Default conversion of markup and quotes
<?php
$input = "<a href='test'>Tom & \"Jerry\"</a>";
echo htmlspecialchars($input), "\n";
Output
&lt;a href=&#039;test&#039;&gt;Tom &amp; &quot;Jerry&quot;&lt;/a&gt;
Comparing quote and document-type flags
<?php
$text = "She said \"hi\" & it's fine";
echo json_encode([
    'default' => htmlspecialchars($text),
    'compat' => htmlspecialchars($text, ENT_COMPAT),
    'noquotes' => htmlspecialchars($text, ENT_NOQUOTES),
    'html5' => htmlspecialchars($text, ENT_QUOTES | ENT_HTML5),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
Output
{"default":"She said &quot;hi&quot; &amp; it&#039;s fine","compat":"She said &quot;hi&quot; &amp; it's fine","noquotes":"She said \"hi\" &amp; it's fine","html5":"She said &quot;hi&quot; &amp; it&apos;s fine"}
double_encode and invalid UTF-8 handling
<?php
$text = "Fish &amp; Chips < 5";
echo htmlspecialchars($text), "\n";
echo htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false), "\n";
$bad = "atta\x80ck";
echo bin2hex(htmlspecialchars($bad, ENT_QUOTES, 'UTF-8')), "\n";
echo bin2hex(htmlspecialchars($bad, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8')), "\n";
Output
Fish &amp;amp; Chips &lt; 5
Fish &amp; Chips &lt; 5

61747461efbfbd636b