Every code HtmlSecurityCheck reports, its message, and what to change so the content passes. The
message is the template with the detail filled in at %s. Codes never change once
released; message wording can, so match on code.
| Code | Message | What to do |
|---|---|---|
not-utf8 |
Content must be UTF-8, this is not: %s |
Convert the text to UTF-8 before the check. A Latin-1 é from an old export is the usual cause: mb_convert_encoding($html, 'UTF-8', 'Windows-1252') when the source encoding is known. Text typed into a form is already UTF-8. |
control-character |
Content contains a control character: %s |
Remove the character; the detail names its byte offset. A NUL comes from a cut-off binary paste or a bad conversion, an escape (\033) from terminal output pasted in. Tab, LF and CR are fine. |
element-not-allowed |
%s is not allowed |
Remove the tag. <script>: put the script in the page template. <?import ...>: bound a behavior in old IE; remove it. A tag inside <!--[if ...]>: the same fix as outside it. <form>, <input>, <button>, <select>, <textarea> and the other form elements: set HtmlSecurityCheck::$allowForms. <style>: $allowStyleElement. <iframe>: $allowEmbeds. <svg>: upload the file and use <img>. A custom element (<my-widget>): use a <div> or <span> with a class. <!DOCTYPE>, <html>, <head>, <body>, <meta>, <title>: paste only the body content, or use the editor's paste-from-Word for a Word document. |
event-handler |
%s= event handler attributes are not allowed |
Remove the attribute. Put the behavior in the page's own script and select the element by a class or id. |
attribute-not-allowed |
The %s attribute is not allowed |
Remove the attribute. data-* or another unknown name: use a class instead. srcdoc: use src with a listed host instead. xmlns:*: remove it; only Microsoft Office namespaces pass. formaction: set HtmlSecurityCheck::$allowForms. style: set $allowStyleAttribute, or move the styles to the site's stylesheet. |
url-scheme-not-allowed |
%s uses a URL scheme that is not allowed |
Use an http:, https:, mailto: or tel: URL, or a relative path. A data: URL passes only as image/... on <img src>; anywhere else, upload the file and link it. A Windows path (\\server\x, `C |
iframe-host |
Embedding frames from %s is not allowed |
Add the host to HtmlSecurityCheck::$iframeHosts, spelled as it appears in the embed code (player.vimeo.com, not vimeo.com), with no scheme, port or path. An <iframe> with no src has nothing to allow: remove it. |
css-not-allowed |
CSS containing %s is not allowed |
Remove the construct. expression(, behavior:, -ms-behavior:, -moz-binding: old IE and Firefox only, nothing renders them now. @import: link the stylesheet from the page template. Any other function or at-rule (image-set(, cross-fade(, @layer) is not on the CSS allowlists: use a plain url() or a listed one, or move the rule to the site's stylesheet. [value^=, [href$=, [name*=: use an exact match ([value=) or a class; on style, src and class these selectors pass. unicode-range: drop it from @font-face. A backslash outside a comment or quoted string, or inside a quoted url(): write the character itself. url(data:...): upload the image and use its path. If the detail is the start of the block rather than a token, the block was too large or too complex to check: shorten it or move it to the site's stylesheet. |
less-than-in-text |
A < where a browser reads text, not tags: %s |
The < is inside <style>, <iframe>, <textarea> or a comment that is not <!-- --> (<?php ... ?>, <!x ...>), where a browser reads it as text but strip_tags() with an allow list or an older DOM library reads it as a tag. In a <textarea> write <. In <style>, write a media query range with min-width or max-width ((max-width: 599px), not (width < 600px)) and a < inside a string as \3C ; a <!-- there passes when its --> is in the same block. In an <iframe> remove the fallback content. A <?...> or <!...> comment: write it as <!-- ... --> or remove it. The detail starts at the < and runs to the end of the text. |
unclosed-markup |
%s is not closed |
Close it: add the missing >, closing quote, --> or end tag (</style>, </iframe>, </textarea>). The detail starts where the open piece began, so the fix is at its end. A cut-off paste is the usual cause. |
not-checked |
Content could not be checked: %s |
PHP's regex engine stopped at pcre.backtrack_limit or pcre.recursion_limit (the detail says which), so the rest of the content was never read. Only a limit set far below PHP's default does this; restore it in php.ini. |
To translate a message, run template through your translation function and put detail
back with sprintf(), then encode the whole result, since the detail holds <, > and
quotes: echo htmlspecialchars(sprintf(t($violation->template), $violation->detail));.
Violation::TEMPLATES holds every template keyed by code, so a translation system can
register them all up front.
Three things to know when reading a result:
- A byte error arrives alone.
not-utf8andcontrol-characterstop the check before the content is read as HTML. Fix the bytes and check again; the other problems are reported then. - A refused tag reports once.
<script onload="x" src="javascript:1"></script>is oneelement-not-allowed, not three errors. Its attributes, and the content of a refused<style>or<textarea>, are not checked, since the tag is going away. - The same problem on five tags is reported once. Errors are deduplicated by code and
detail: five
<script>tags are one error, five differenton*attributes are five. The list stops atHtmlSecurityCheck::$maxErrorsdistinct problems (50 by default), andunclosed-markupis last, except inside a<!--[if ...]>comment, where it ends at the comment's end.