Allowing Additional HTML Through wp_kses
The filter receives a context, and the context is the whole point: widening the post context affects every call to wp_kses_post across the site, including places that trusted it to strip exactly what you just allowed.
The filter changes what a named context allows, and the post context is used everywhere. wp_kses_allowed_html runs inside wp_kses_allowed_html(), which wp_kses_post(), wp_kses_data() and every wp_kses() call made with a context name go through. Its two arguments are the allowed-tags array and the context string — post, user_description, strip, or the name of the current filter. Adding a tag for post adds it for post content, comments from trusted roles, widget text, and every plugin that sanitises with wp_kses_post(), because they all asked for the same context.
Allowing an iframe for the post context
add_filter( 'wp_kses_allowed_html', 'ti_allow_iframe', 10, 2 );
function ti_allow_iframe( $allowed, $context ) {
if ( 'post' !== $context ) {
return $allowed;
}
$allowed['iframe'] = array(
'src' => true,
'width' => true,
'height' => true,
'title' => true,
'loading' => true,
'allow' => true,
'allowfullscreen' => true,
'frameborder' => true,
);
return $allowed;
}
Attributes are keys set to true inside the tag's array. An entry of 'iframe' => array() allows the element and strips every attribute, which leaves an empty frame — the most frequent "I allowed it and it still breaks" outcome. Global attributes such as class, id, style and data-* are merged into core's own default tags when WordPress loads, not into tags you add later: a tag added through the filter gets exactly the attributes you list, so add 'class' => true and 'id' => true to the iframe array if the embed relies on them.
Note what is not in that list: onload, srcdoc, or anything that executes. kses is the layer that stops a contributor's post from carrying script, and an attribute that can carry script hands that capability to every role whose content goes through the post context.
The contexts
| Context | Used by | Default scope |
|---|---|---|
post |
wp_kses_post(), post content, widget text |
Rich content: headings, lists, tables, media; forms are not included |
user_description |
User biography | Links, emphasis, a short list |
pre_user_description |
Biography before saving | Same as above |
strip |
wp_filter_nohtml_kses() |
Nothing |
Name of the current filter, e.g. pre_comment_content or pre_term_description |
wp_kses_data() and the wp_filter_kses callbacks |
The small inline set: links, emphasis, a few more |
| Array | wp_kses( $html, $array ) |
Exactly the array — the filter is not consulted |
Targeting post is right for content. Term descriptions and comments are not the post context; they pass through wp_filter_kses with the small default set and their own filter name as context. Targeting the biography context to allow an image is a decision about every user profile on the site, and on a membership site that means user-uploaded markup on public pages.
Who bypasses the filter
Users with the unfiltered_html capability — administrators and editors on a single site, super admins only on multisite — have their content saved without kses. The filter never runs for them, which is why an administrator cannot reproduce an author's stripped embed. Testing a kses change means testing as an author. The DISALLOW_UNFILTERED_HTML constant removes the capability from everyone, at which point the filter governs administrators too — a sensible hardening on sites with many admins, and one of the controls that belongs alongside DISALLOW_FILE_EDIT in a security baseline.
Data attributes and custom elements
data-* is the one wildcard kses understands, and it is already part of the global attributes on every default post tag, so a data- attribute on a div or span survives wp_kses_post() without any filter. On a tag you add yourself, list it:
$allowed['iframe']['data-*'] = true;
There is no general prefix wildcard — aria-* does not work — so each aria- attribute is listed by name. Custom elements — a <my-widget> tag from a web component — are allowed like any other tag, but kses lowercases tag names and strips anything it does not know, so the tag and every attribute must be listed.
Where allowing is the wrong answer
Allowing script, style blocks, or event-handler attributes through kses is handing those abilities to every role that writes content. If a specific embed needs them, the better paths are a shortcode or block rendered server-side with the markup hard-coded, or an oEmbed provider registration that lets WordPress produce the iframe itself. Both keep the output outside the sanitiser without widening it. The same reasoning applies to forms: kses strips <form> and <input> from the post context by design, and a contact form is a plugin's job, not an allow-list change.
Verify
Log in as an author, paste the embed into a post, save, and view it. The iframe should render with its src intact. Then paste the same embed with an onload attribute: it should render without it. Finally, check one place you did not intend to change — a text widget, or any plugin output sanitised with wp_kses_post() — to confirm the allowance is acceptable there too, since it applies. If the author's embed is still stripped, confirm the context by logging $context for one request; a plugin calling wp_kses() with its own array is not the post context and needs its own filter branch.
Frequently asked
- Administrators on a single site hold the unfiltered_html capability, and content from such users is not passed through wp_kses at all. Editors and authors do not hold it, so their content is filtered against the post context. The filter changes what the post context allows; it does not grant the capability.
- No. When wp_kses() receives an array, it uses that array directly and never consults the filter. Only calls made with a context name — wp_kses_post(), wp_kses_data(), wp_kses( $html, 'post' ) — reach it, which is also why the filter cannot widen a plugin that sanitises with its own explicit array.
- Attributes are allowed per tag, as keys set to true inside the tag's array. Allowing iframe with an empty array allows an iframe with no attributes, and kses removes src, width and the rest.