Skip to main content
PHP HTML Editor

Clean HTML from a rich text editor in PHP and stop XSS

Why the browser cannot be trusted with this, and how to clean the HTML before you save it.

HTML with a script goes in; the same text without the script comes out

A rich text editor sends HTML to your server, and you later show that HTML to other people. If someone can get a script into it, that script runs in your visitors' browsers, on your site, with their logged-in session. This is called stored cross-site scripting, or XSS, and it is the classic risk of any site with an HTML editor.

Why the editor cannot solve this alone

The editor in the browser can clean what people type and paste. But an attacker does not need to use your editor. They can send your form any text they like, with a small script or a browser's developer tools. So cleaning in the browser is a convenience, and the real check has to be on the server, after the form arrives and before you save.

Why strip_tags() is not the answer

PHP's strip_tags() removes tags, and its list of allowed tags keeps all their attributes. So this passes straight through, and runs when someone moves the mouse over it:

<?php
echo strip_tags('<p onmouseover="steal()">Hello</p>', '<p>');
// <p onmouseover="steal()">Hello</p>
?>

htmlspecialchars() has the opposite problem. It is safe, but it shows your users' formatting as literal tags. And filters built on regular expressions are famous for missing cases. HTML needs a real parser.

The fix: one call

<?php
require_once __DIR__ . '/richtexteditor/phphtmleditor/autoload.php';

$editor = new PhpHtmlEditor\Editor('Article');
$editor->handleRequest();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $html = $editor->posted();    // cleaned; use this, not $_POST['Article']
    // save $html
}
?>

On the classic API it is RichTextEditor::GetPostedHtml("Editor1").

posted() parses the HTML with PHP's DOM extension and keeps only what is on an allowlist. Here is an actual result:

Sent to the serverWhat you save
<p onclick="steal()">Hello</p><p>Hello</p>
<script>steal()</script>(nothing)
<a href="javascript:steal()">Offer</a><a>Offer</a>
<img src="/a.jpg" alt="Cat">Unchanged

What is removed

  • scripts, forms, SVG, <object> and <embed>,
  • every on... event attribute,
  • javascript: and other script addresses,
  • an id or name that could overwrite parts of your page's scripts (known as DOM clobbering).

Embedded frames are allowed but always sandboxed. Headings, lists, tables, links, pictures and normal formatting all survive, so what your users wrote still looks the way they wrote it.

Your own extra rules

The policy can remove more. To drop tables from user comments, add them to the blacklist:

<security name="TagBlackList">script,style,link,applet,bgsound,meta,base,basefont,frameset,frame,form,table</security>

This adds to the allowlist's protection and can never weaken it.

Old content

Cleaning on save protects what is saved from now on. If your database holds HTML from earlier, run it through the same cleaner when you display it, or once in a batch:

<?php
$safe = $editor->sanitize($oldHtml);
?>

Then keep using posted(), and remember that this protects HTML from the editor. Everything else you print, such as a user name, still needs htmlspecialchars().

Read next