Skip to content

Content Sanitizer ​

Scripts, event handlers and unsafe links taken out of the HTML that authors and contributors save; administrators and editors keep raw HTML.

kitloom/content-sanitizer · admins: kitloom/content-sanitizer-filament, kitloom/content-sanitizer-nova

Video lessons ​

Recorded in the Filament admin; the Nova admin works the same way.

A page is HTML, and HTML can run code. On a site with one author that is fine: you write what you want on your own pages. On a site where other people write too — authors, contributors, shop staff, translators, or an AI agent working for any of them — someone could put a <script> or an onerror= into a post. Nothing happens to them; it runs when you open the post as an administrator, with your session. WordPress stops that with its filter called kses: only users who may write unfiltered HTML keep their markup as written, everyone else has it cleaned when they save. This module does the same for kitloom.

Whose HTML is cleaned ​

Everyone's except the holders of the permission Write unfiltered HTML (unfiltered_html, WordPress's own capability). As on a single WordPress site, Administrator and Editor hold it; Author, Contributor, Subscriber and every role you or a module made do not. Give it to another role, or take it from Editor, on Users → Roles in either admin, the way you change any other permission. Content Sanitizer → Settings shows who holds it now.

Imports, the demo and console commands write as the site itself and are never cleaned: a WordPress export comes over as it was.

What is cleaned, when such a user saves it:

  • a post's or a page's content and excerpt (also products, patterns and any other type that holds HTML);
  • a category's, a tag's or any term's description;
  • a WYSIWYG custom field, at any depth of groups, repeaters, flexible content and clones;
  • a translation's content and excerpt (kitloom/languages), also a user's translated biography.

Only what they changed is cleaned. An author who moves your post to another category, or saves the page with your values in its fields, leaves your markup alone.

Whatever screen they save from — either admin, the REST API, an AI agent, a spreadsheet import — the same rules hold.

What stays and what goes ​

What a writer needs stays: headings, paragraphs, lists, links, quotes, tables, figures and captions, images with their sizes (srcset), video and audio with their sources and subtitles, details/summary, the block editor's markup with its comments, colours, alignments and spacing, shortcodes ([gallery], [embed]), the Read more and page-break markers. Text and entities (&nbsp;, &amp;) are kept exactly as typed: a clean save changes nothing, byte for byte, so revisions show no difference.

What runs goes:

  • <script>, <style>, <template>, <noscript>, forms, <object>, <embed>, SVG and MathML drawn inside the text;
  • event handlers (onclick, onerror, …) and every other attribute that is not on the list, such as Bootstrap's data-bs-*, htmx's hx-* or Alpine's x-* — a script of the site reads those and runs what they say;
  • links and images whose address runs code: javascript:, vbscript:, data: (also hidden behind entities, spaces or mixed case);
  • frames (<iframe>) from any site not on your list;
  • inline styles that load something, run something or lay a box over the page (position, z-index, url() but a background picture, expression()). The rest of the style stays.

An element that is not on the list is unwrapped: the tag goes, its text stays. An excerpt and a term's description keep less — the words and their emphasis and links — as WordPress allows less there.

A text that a browser could read differently on a second look (a known parser trick) is not stored as markup at all: it is kept as plain, escaped text.

When something was removed ​

What happens when an author saves a script

The person is told what went, by name:

  • in the Filament admin, a notice right after the save: Removed from what you saved, as your role may not store it: script, onerror.
  • in Nova, the same words in the notifications (the bell): Nova's form answers a save with its own words;
  • an AI agent gets the names in its tool's answer (removed), so it can tell its user.

Nothing is refused: the save goes through without what was removed. An author who pastes from Word can still save.

Settings ​

Stop authors saving scripts in Laravel: the content sanitizer

Content Sanitizer → Settings (needs Manage the content sanitizer, administrators):

SettingWhat
Clean HTML of users without «Write unfiltered HTML»Off: everything is stored as written, as before the module.
Frames allowed fromOne host a line, over HTTPS. A path narrows it: www.google.com/maps allows Maps embeds and nothing else from Google. Empty: no frames at all. Starts with YouTube (also youtube-nocookie.com), Vimeo and Google Maps.
Keep safe inline stylesOff: every style attribute goes, also the block editor's colours.
More data- attributes to keepOne name a line, such as data-gallery, for a script of your theme. Never one a script reads to run code.

Content from before ​

Posts written before the module was on, or brought over from WordPress, are not touched until a user without Write unfiltered HTML saves them. To see what is there, run:

bash
php artisan content-sanitizer:scan          # what would change, where, whose, and what would go
php artisan content-sanitizer:scan --fix    # clean it — back up the database first

--fix asks before it writes (--force does not ask) and cleans as the site itself. It cleans an administrator's own embeds too, which is why it is never done on its own.

Configuration ​

config/kitloom-content-sanitizer.php (php artisan vendor:publish --tag=kitloom-content-sanitizer-config):

KeyWhat
enabled (CONTENT_SANITIZER)false: nothing is cleaned, whatever the setting says
elements, short_elementsthe elements that stay, with their attributes; the shorter list for excerpts and descriptions
global_attributes, data_attributesattributes every element keeps; the data- names kept
schemesthe address schemes allowed: http, https, mailto, tel
iframe_hostswhere the settings page's list of frames starts
styles, css_properties, display_valuesinline styles on or off before the owner chooses; the properties kept
skip_post_typestypes whose content is CSS or JSON, never cleaned
filament.register, nova.registerfalse: no section in that admin; HTML is cleaned all the same

Good to know ​

  • Biographies. A user's Biographical Info is shown on their author page as HTML only when they may write unfiltered HTML; anyone else's is shown as text. Since administrators and editors hold the permission now, their biographies show their links and emphasis.
  • Widgets (Custom HTML, block widgets) are not cleaned yet: only users who may edit the theme's options change them, administrators by default.
  • Comments are not cleaned here: the site draws every comment as plain text.
  • Header and footer snippets have a permission of their own (kitloom/header-footer).

For developers ​

The core (kitloom/wp-schema) asks a Kitloom\WpSchema\Contracts\StoredHtmlFilter for every HTML text a user without unfiltered_html writes: Content\StoredHtmlGuard::clean($html, $context) decides who is filtered, Content\StoredHtml says what the text is (the actor, the kind, the record, its type), and the removals of the request are kept in Content\HtmlRemovals. Without a module the filter keeps everything (Content\KeptHtml); this module binds its own, SanitizingFilter, over HtmlSanitizer — PHP's HTML5 parser (Dom\HTMLDocument) and an allowlist written from the HTML standard, no outside library. A module that stores HTML a user wrote passes it through StoredHtmlGuard::clean() with the actor.

Read more ​

kitloom — modules and themes for Laravel.
Contact · Pricing · Services · License · Refunds · Privacy · Community · YouTube · [email protected]