htmlspecialchars($text) converts &, <, >, " and ' into &, <, >, " and '. Call it on every piece of user input you print into a page, and the input is shown as text instead of being read as HTML.
The block prints the same comment twice, once raw and once escaped. Run it and compare the two lines on the Page tab, then type your own HTML into the form, for example <h1>big</h1> or <img src=x>, and press Show.
On the raw line the browser obeys the tags: the bold word is bold, and a visitor who types <script> gets their script run in every other reader's browser. That attack is called cross-site scripting (XSS). On the escaped line the same characters arrive as <b> and the browser draws them as text. Switch to the Output tab to see the entities PHP actually printed.
What htmlspecialchars converts
Five characters, nothing else. Letters, accents and emoji pass through unchanged.
& is on the list because it starts every entity: if it were left alone, a comment that mentions < would be displayed as <.
Escape attributes, not only text
User input inside an attribute needs escaping just as much. Without it, a quote in the value closes the attribute and the rest of the input becomes new attributes. Here the "name" sneaks in a style attribute; run it and look at the two boxes.
In the unsafe box the browser sees value="Ada" followed by a new style attribute, so the box turns red and shows only Ada. An attacker would write onfocus="..." there instead of style, and their code would run. In the safe box every " became ", so the whole string stays inside value and is shown as typed.
Since PHP 8.1 the default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, so single quotes are escaped too and attributes written with '...' are safe. Older code often passes ENT_QUOTES by hand, and on PHP 7 and earlier that was required:
A short helper for templates
Writing htmlspecialchars($x, ENT_QUOTES, 'UTF-8') dozens of times in a template is noisy, so most projects wrap it in a one-letter function. Template engines like Twig and Blade do the same thing automatically for every {{ $var }}.
The ?string type and ?? '' matter: passing null to htmlspecialchars() is deprecated since PHP 8.1, and your own PHP would print a deprecation notice for every user without a bio.
Double encoding and htmlspecialchars_decode
If a value is escaped twice, the reader sees the entities: & becomes & the first time and &amp; the second, which the browser shows as &. It usually means the value was escaped when it was saved and again when it was printed. Pass double_encode: false to leave existing entities alone, and use htmlspecialchars_decode() to go back.
The real fix is to store raw text and escape only on output. double_encode: false is for text that already contains entities you did not create, like an imported feed.
htmlspecialchars vs htmlentities vs strip_tags
These three are often confused. The block runs all of them on the same input and shows, for each, what PHP prints and what the browser makes of it:
htmlspecialchars()escapes the five HTML characters. Use it for any text you print into HTML.htmlentities()also turnséintoé. It was useful when pages were not UTF-8; today it only makes the source harder to read.strip_tags()deletes tags and keeps their text. It is for turning HTML into plain text (an email preview, a meta description), not for security: the last row shows that an allowed<b>keeps itsonclick, and text placed inside an attribute is not touched at all.
Where htmlspecialchars is not enough
htmlspecialchars() is the right escape for HTML text and quoted attributes. Other places in a page have other rules:
- In a URL,
http_build_query()orurlencode()encodes the value;htmlspecialchars()then makes the&between parameters valid HTML. - In JavaScript,
json_encode()produces a valid JS value, andJSON_HEX_TAGturns<and>into\u003Cand\u003E, so a</script>in the data cannot close the script tag. - Never print user input into a
hrefwithout checking the scheme:htmlspecialchars('javascript:alert(1)')is unchanged and still runs when clicked. Accept onlyhttpandhttpsURLs, as shown on the filter_var page.
For form handling that puts all of this together, see PHP forms.
Frequently Asked Questions
What does htmlspecialchars do in PHP?
It replaces &, <, >, " and ' with &, <, >, " and '. The browser then displays those characters instead of reading them as HTML, so <script> typed into a form is shown as text and never runs.
What is the difference between htmlspecialchars and htmlentities?
htmlspecialchars() converts only the five characters that are special in HTML. htmlentities() converts every character that has a named entity too, so café becomes café. With UTF-8 pages both are equally safe, and htmlspecialchars() keeps the output readable, so it is the usual choice.
Do I still need ENT_QUOTES in PHP 8?
Not for safety: since PHP 8.1 the default flags are ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, so single quotes are escaped too. Many codebases still pass ENT_QUOTES, 'UTF-8' explicitly so the call behaves the same on older versions and is obvious to readers.
Should I use htmlspecialchars on input or output?
On output. Store and validate the raw value, and escape it at the moment you print it into HTML. Escaping on input stores < in your database, breaks lengths and searches, and leads to double escaping like &lt;.
Is strip_tags enough to prevent XSS?
No. strip_tags() removes tags but its allowed-tags parameter keeps their attributes, so <b onclick="..."> survives, and it does nothing for text placed inside an attribute. Use htmlspecialchars() when printing user input.