Appearance
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/ · admins: kitloom/, kitloom/
Video lessons
Recorded in the Filament admin; the Nova admin works the same way.
- What happens when an author saves a script
- Stop authors saving scripts in Laravel: the content sanitizer
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_, 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 ( , &) 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'sdata-, htmx'sbs- * 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
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
Content Sanitizer → Settings (needs Manage the content sanitizer, administrators):
| Setting | What |
|---|---|
| Clean HTML of users without «Write unfiltered HTML» | Off: everything is stored as written, as before the module. |
| Frames allowed from | One host a line, over HTTPS. A path narrows it: www. allows Maps embeds and nothing else from Google. Empty: no frames at all. Starts with YouTube (also youtube-), Vimeo and Google Maps. |
| Keep safe inline styles | Off: every style attribute goes, also the block editor's colours. |
| More data- attributes to keep | One name a line, such as data-, 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-- asks before it writes (-- 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/ (php artisan vendor:):
| Key | What |
|---|---|
enabled (CONTENT_) | false: nothing is cleaned, whatever the setting says |
elements, short_ | the elements that stay, with their attributes; the shorter list for excerpts and descriptions |
global_, data_ | attributes every element keeps; the data- names kept |
schemes | the address schemes allowed: http, https, mailto, tel |
iframe_ | where the settings page's list of frames starts |
styles, css_, display_ | inline styles on or off before the owner chooses; the properties kept |
skip_ | types whose content is CSS or JSON, never cleaned |
filament., nova. | false: 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\ for every HTML text a user without unfiltered_ writes: Content\ decides who is filtered, Content\ says what the text is (the actor, the kind, the record, its type), and the removals of the request are kept in Content\. Without a module the filter keeps everything (Content\); this module binds its own, Sanitizing, over Html — PHP's HTML5 parser (Dom\) and an allowlist written from the HTML standard, no outside library. A module that stores HTML a user wrote passes it through Stored with the actor.