Discover secure and effective HTML sanitization techniques in PHP beyond strip_tags(). Learn how to safeguard your web applications against XSS and other vulnerabilities using advanced tools like HTML Purifier and AntiSamy.
You inherit a form. Whoever built it wrapped every user field in strip_tags() and marked the ticket “sanitized.” It looks tidy, it passed review, and it is quietly wrong. strip_tags() was never a security control, and treating it like one is how XSS bugs slip into shipped code.
Here’s the honest version of what these functions do, when each one is the right call, and where strip_tags() will burn you.
Why strip_tags() isn’t a sanitizer
strip_tags() removes HTML and PHP tags from a string. That’s the whole job. It reads like a safety net, but it wasn’t built to defend against attackers, and it has real gaps:
- It ignores attributes. Pass an allowlist like
'<a><img>'and the tags stay, but so does everything inside them. A kept<img>can still carryonerror="...".strip_tags()never looks. - It leaves the text behind. It strips the tag, not the content between the tags.
<script>alert(1)</script>becomesalert(1), which is fine only if you escape it afterward. - It has no context. It doesn’t know whether the result lands in an attribute, a URL, or a script block. Same output everywhere, safe in none of them by itself.
None of that makes strip_tags() useless. It makes it a formatting tool, not a security boundary. The mistake is asking it to do a job it was never designed for.
What you’re actually defending against
The reason this matters is cross-site scripting. If unescaped user input reaches the browser as markup, an attacker can run script in your users’ sessions:
- Cross-site scripting (XSS): injected scripts run in another user’s browser. That means stolen sessions, hijacked accounts, and data walking out the door.
- Corrupted data: weak sanitization lets malformed or hostile content get stored, then served back to everyone who views it.
- Lost trust: one public breach costs you credibility you spent years earning.
The tools that actually do the job
There’s no single “better than strip_tags()” function, because there’s no single problem. Pick by what you’re trying to do:
htmlspecialchars(): escapes< > & " 'into entities so the browser prints them as text instead of running them. This is your default for echoing user input into HTML.htmlentities(): same idea, but encodes every character with an entity equivalent.- HTML Purifier: when you need to keep some HTML (comments, rich text) but strip anything dangerous. A real allowlisting sanitizer, not an escaper.
filter_var(): for non-HTML values like emails and URLs.
The important distinction: escaping and sanitizing are different jobs. htmlspecialchars() neutralizes markup so it displays as plain text. HTML Purifier lets safe markup survive and removes the rest. strip_tags() does neither reliably. Reach for the one that matches your goal, not the one you typed last time.
Escaping output with htmlspecialchars() and htmlentities()
When you’re printing user input back into a page and you want it to show up as text, escape it. Both functions turn HTML-special characters into their entity forms so the browser can’t interpret them as code.
Example 1: htmlspecialchars()
Here the script tags come out as visible text, not an executable element.
<?php
$string = '<script>alert("XSS Attack!")</script>';
$sanitized_string = htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
echo $sanitized_string;
// Output: <script>alert("XSS Attack!")</script>
?>Pass ENT_QUOTES so both single and double quotes are encoded, and name the charset explicitly. Skip either and you leave yourself a gap when the value ends up inside an attribute.
Example 2: htmlentities()
Same protection, heavier hand: it encodes every applicable character.
<?php
$string = '<a href="http://example.com">Link</a>';
$sanitized_string = htmlentities($string, ENT_QUOTES, 'UTF-8');
echo $sanitized_string;
// Output: <a href="http://example.com">Link</a>
?>For most output, htmlspecialchars() is enough. Reach for htmlentities() when you specifically need the fuller character encoding.
Keeping safe HTML with HTML Purifier
Escaping is the wrong tool when the content is supposed to keep formatting: a comment with a link, a rich-text field with bold and lists. You don’t want to print the tags as text, you want to allow the safe ones and drop the rest. That’s what HTML Purifier does, and it does it against a real parser instead of guesswork.
Installing HTML Purifier
composer require ezyang/htmlpurifierPull it in with Composer and let it own the “which HTML is allowed” decision.
HTML Purifier: basic usage
With the default config, the script tag is removed and the real paragraph survives.
<?php
require_once 'vendor/autoload.php';
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$dirty_html = '<p><script>alert("XSS")</script>This is a paragraph.</p>';
$clean_html = $purifier->purify($dirty_html);
echo $clean_html;
// Output: <p>This is a paragraph.</p>
?>Notice the difference from strip_tags(): Purifier removes the script and its contents, because it understands the element. strip_tags() would have left alert("XSS") sitting in the text.
HTML Purifier: tighter config
The default policy is sensible, but you can narrow it. Allowlist exactly the tags and attributes you want and let Purifier strip everything else, dangerous attributes included.
Example 3: configuring HTML Purifier
Here only p, b, and a[href] are permitted, so a hostile onclick never makes it through.
<?php
$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,a[href]');
$config->set('URI.SafeIframeRegexp', '%^https://www.youtube.com/embed/%');
$purifier = new HTMLPurifier($config);
$dirty_html = '<p><a href="http://example.com" onclick="stealCookies()">Click me</a></p>';
$clean_html = $purifier->purify($dirty_html);
echo $clean_html;
// Output: <p><a href="http://example.com">Click me</a></p>
?>This is the control strip_tags() can’t give you: the link stays, the attack attribute is gone.
Sanitizing non-HTML input with filter_var()
Not every value is HTML. For emails and URLs, filter_var() has purpose-built filters. Treat these as cleanup, and still validate (with the matching FILTER_VALIDATE_* filter) before you trust the result.
Example 4: sanitizing an email address
The email filter strips characters that don’t belong in an address.
<?php
$email = '[email protected]';
$sanitized_email = filter_var($email, FILTER_SANITIZE_EMAIL);
echo $sanitized_email;
// Output: [email protected]
?>Example 5: sanitizing a URL
Same idea for a URL: strip invalid characters, then validate before you use it.
<?php
$url = 'http://example.com';
$sanitized_url = filter_var($url, FILTER_SANITIZE_URL);
echo $sanitized_url;
// Output: http://example.com
?>Rolling your own: custom functions
Sometimes the built-ins don’t line up with a specific requirement and you write your own helper. Fine, as long as you’re clear-eyed about what each piece does.
Example 6: a custom sanitizer
This one pairs strip_tags() with htmlentities(). The order matters, and so does understanding it.
<?php
function custom_sanitize($input) {
$input = strip_tags($input, '<b><i><a>');
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');
return $input;
}
$input = '<b>Hello</b> <script>alert("XSS")</script>';
$sanitized_input = custom_sanitize($input);
echo $sanitized_input;
// Output: <b>Hello</b>
?>One caveat worth stating plainly: strip_tags() drops the <script> tag but leaves its text, alert("XSS"), in the string. The output comment above is a simplification. What actually saves you here is the htmlentities() pass, which escapes that leftover text into harmless entities. The escaping does the security work; strip_tags() is only tidying up the markup. Take away the second line and this “sanitizer” leaks.
A note on regular expressions
You’ll see advice to strip tags with regex. Be careful. HTML is not a regular language, and hand-rolled patterns miss malformed or nested cases that a browser will still happily execute. If you go this route, always escape afterward, and prefer a real parser (like Purifier) for anything that matters.
Example 7: regex plus escaping
This removes obvious script blocks, then encodes what’s left. The encoding is what makes it safe, not the pattern.
<?php
function regex_sanitize($input) {
// Remove all script tags
$input = preg_replace('/<script\b[^>]*>(.*?)<\/script>/is', '', $input);
// Encode remaining HTML entities
$input = htmlentities($input, ENT_QUOTES, 'UTF-8');
return $input;
}
$input = '<script>alert("XSS")</script><p>This is a paragraph.</p>';
$sanitized_input = regex_sanitize($input);
echo $sanitized_input;
// Output: <p>This is a paragraph.</p>
?>Don’t lean on the regex alone. A single obfuscated payload it didn’t anticipate is all it takes.
OWASP PHP AntiSamy
If you want policy-file control over exactly which tags, attributes, and CSS properties are allowed, the PHP port of OWASP AntiSamy offers it. Be honest with yourself first: the PHP port is far less actively maintained than HTML Purifier, so check its current state before you bet a project on it. For most PHP work, Purifier is the safer default.
Installing OWASP PHP AntiSamy
Install it with Composer.
composer require owasp/antisamy-phpOWASP AntiSamy: basic usage
AntiSamy scans input against a policy file (antisamy.xml) that spells out the allowed tags, attributes, and CSS.
<?php
require_once 'vendor/autoload.php';
use Owasp\AntiSamy\AntiSamy;
use Owasp\AntiSamy\Policy;
$antisamy = new AntiSamy();
$policy = Policy::getInstance('antisamy.xml');
$input = '<b>Hello</b> <script>alert("XSS”)</script>';
$scanResult = $antisamy->scan($input, $policy);
$clean_html = $scanResult->getCleanHTML();
echo $clean_html;
// Output: <b>Hello</b>
?>OWASP AntiSamy: custom policy
The policy file is where the power is: name the exact elements and attributes you’ll accept, and everything else is dropped.
Example 8: a custom policy
Here a custom policy keeps the link but strips the onclick.
<?php
$policy = Policy::getInstance('custom-antisamy.xml');
$input = '<a href="http://example.com" onclick="stealCookies()">Click me</a>';
$scanResult = $antisamy->scan($input, $policy);
$clean_html = $scanResult->getCleanHTML();
echo $clean_html;
// Output: <a href="http://example.com">Click me</a>
?>If you’re on WordPress
One thing the original framing skips: on WordPress you don’t reach for HTML Purifier first. Core ships wp_kses() and wp_kses_post(), which are allowlisting sanitizers built for exactly this. wp_kses_post() permits the same HTML the post editor allows; wp_kses() lets you pass your own tag and attribute allowlist. For escaping on output, use esc_html(), esc_attr(), and esc_url(). Same rules as above, WordPress just hands you the tools.
Picking the right tool
The short version, so you can stop guessing:
- Printing user input as text? Escape it with
htmlspecialchars()(oresc_html()in WordPress). - Need to keep some HTML but drop the dangerous parts? Use a real sanitizer: HTML Purifier, AntiSamy, or
wp_kses(). - Handling an email or URL? Sanitize with
filter_var(), then validate. - Reaching for
strip_tags()as your security layer? Don’t. It’s a formatting helper, and it always needs escaping behind it.
The goal isn’t to run more functions. It’s to match the tool to the job and never confuse “the tags are gone” with “this is safe to render.” Revisit these choices as your app grows and as new attack patterns show up; sanitization is a habit, not a one-time checkbox.
Further reading:



Great content!
Thanks John, hope you find more useful content on our site!