HTML Escaping

Last updated Jan 17, 2024

Introduction

ACF 5.10 introduced an important security feature, HTML escaping. First introduced as an experimental feature in ACF 5.9.6, it ensures that all HTML content rendered by ACF is passed through the WordPress wp_kses() function.

By running all HTML content through this WordPress core function, ACF ensures that any HTML it renders is not vulnerable to Cross Site Scripting attacks.

It’s important to note that this only affects HTML content rendered by ACF in your WordPress dashboard or any front-end forms rendered through acf_form(). This will not affect field values loaded through API functions such as get_field(). We don’t make any assumptions about where you are using your field values within your theme and do not escape to them as a result.

From ACF 6.2.5, HTML escaping also applies to the values output by the ACF Shortcode and from ACF 6.2.7, it applies to the_field and the_sub_field

Implementation

This new ACF HTML escaping system introduces a new escaping function, acf_esc_html(), which is now used throughout the plugin wherever HTML is rendered by ACF. This function takes the content to be rendered and passes it to the wp_kses() function, returning the result. It also passes a context string of acf.

/**
 * Sanitizes text content and strips out disallowed HTML.
 *
 * This function emulates wp_kses_post() with a context of "acf" for extensibility.
 *
 * @date    16/4/21
 * @since   5.9.6
 *
 * @param   string $string
 * @return  string
 */
function acf_esc_html( $string = '' ) {
    return wp_kses( (string) $string, 'acf' );
}

By passing the ACF content through wp_kses(), WordPress will strip out any disallowed HTML tags or tag attributes. The list of allowed tags is managed by the WordPress wp_kses_allowed_html() function. By default, WordPress ships with a bunch of predefined allowed tags and their allowed attributes. Below is an example of the allowed button tag, as well as its allowed attributes.

[button] => Array
    (
        [disabled] => true
        [name] => true
        [type] => true
        [value] => true
        [aria-describedby] => true
        [aria-details] => true
        [aria-label] => true
        [aria-labelledby] => true
        [aria-hidden] => true
        [class] => true
        [id] => true
        [style] => true
        [title] => true
        [role] => true
        [data-*] => true
    )

If an HTML element appears in the content passed to wp_kses() that is not in the list of allowed tags, or an attribute on a tag not in the allowed list, it will be stripped from the content.

Customization

Passing the custom context of acf to wp_kses() allows for 3rd party customization of the allowed HTML tags and attributes. If you have a specific HTML tag or tag attribute that you want to allow, you can add it to the list of allowed tags, using the wp_kses_allowed_html filter hook in PHP.

For example, you can use this filter in your theme functions.php or a custom plugin, to enable an iframe tag:

add_filter( 'wp_kses_allowed_html', 'acf_add_allowed_iframe_tag', 10, 2 );
function acf_add_allowed_iframe_tag( $tags, $context ) {
    if ( $context === 'acf' ) {
        $tags['iframe'] = array(
            'src'             => true,
            'height'          => true,
            'width'           => true,
            'frameborder'     => true,
            'allowfullscreen' => true,
        );
    }

    return $tags;
}

Here is another example, explicitly allowed the svg and path tags:

add_filter( 'wp_kses_allowed_html', 'acf_add_allowed_svg_tag', 10, 2 );
function acf_add_allowed_svg_tag( $tags, $context ) {
    if ( $context === 'acf' ) {
        $tags['svg']  = array(
            'xmlns'       => true,
            'fill'        => true,
            'viewbox'     => true,
            'role'        => true,
            'aria-hidden' => true,
            'focusable'   => true,
        );
        $tags['path'] = array(
            'd'    => true,
            'fill' => true,
        );
    }

    return $tags;
}

It is important to remember that any allowed tags you add could have security implications, so be sure only to allow tags that are considered safe. An example of a possible unsafe tag is the script tag.

The WordPress functionality that strips HTML tags and tag properties it deems unsafe, also strips out CSS properties that are considered unsafe, like the display CSS property. In some cases, you may find the need to define and enqueue an inline style in the WordPress dashboard, to apply such a property to your ACF instruction messages.

<div style="display: flex">

Because display is stripped, this property would be stripped from the div tag.

Similarly to how you can allow specific HTML tags, you can also allow specific CSS properties, using the safe_style_css filter:

add_filter( 'safe_style_css', 'add_display_to_safe_css', 10, 1 );
function add_display_to_safe_css( $css_attributes ) {
    $css_attributes[] = 'display';

    return $css_attributes;
}

Please note that while it is possible to do this, we don’t recommend it, as it could expose your site to security vulnerabilities.

HTML Escaping in ACF Blocks

In Advanced Custom Fields PRO version 5.12 and above, WordPress will run it’s default wp_kses_post() sanitization on ACF blocks for admin users without the unfiltered_html capability. In some circumstances, this can result in HTML being removed from the block content — and therefore not rendered on the frontend — that wouldn’t have been removed in versions previous to 5.12.

If a user that doesn’t have the unfiltered_html capability needed to insert HTML into ACF blocks that is being removed by wp_kses_post(), you can add support for specific HTML tags by using the same wp_kses_allowed_html filter as shown above, except with the “post” context:

add_filter( 'wp_kses_allowed_html', 'acf_add_allowed_iframe_tag', 10, 2 );
function acf_add_allowed_iframe_tag( $tags, $context ) {
    if ( $context === 'post' ) {
     $tags['iframe'] = array(
         'src'          => true,
         'height'       => true,
         'width'        => true,
         'frameborder'  => true,
         'allowfullscreen' => true,
     );
    }

    return $tags;
}

Alternatively, you could manually grant the user the unfiltered_html capability.