Useful Snippets

Welcome!


This blog is used to collect useful snippets related to Linux, PHP, MySQL and more. Feel free to post comments with improvements or questions!

Are your smart devices spying on you? Make better purchasing choices and find products that respect your privacy at Unwanted.cloud

RSS Latest posts from my personal blog


Subscribe to RSS feed


Escaping and securing Advanced Custom Fields output

Stanislav KhromovStanislav Khromov

Do you use get_field() to output values saved in Advanced Custom Field in your theme or plugin without escaping it first? Maybe something like this?

echo get_field('my_field');

If so, your site is likely vulnerable to cross-site scripting attacks (XSS) and other malicious hijacking by users who have access to your ACF forms! (Through the admin backend or frontend, if using acf_form())

ACF author Elliot Condon is not completely sure whether ACF escapes input when saving posts to database. However, it doesn’t.

Who is affected by this?

The issue arises from potentially harmful data collected on the front or backend using ACF forms, that is then displayed without proper escaping.

If you do not use frontend forms via acf_form()
In this case you are not vulnerable to unauthenticated frontend attacks. However, anyone who has access to /wp-admin and can edit ACF forms that are output on the front or backend, can introduce XSS vulnerabilities unless you escape the output.

Aside from Administrator accounts getting hacked (in which case you are probably not in a great position anyway), you may also have less privileged users that still have access to ACF forms. (For example, using Author or Editor roles.). For these users, /wp-admin is not an “admin” panel in the traditional sense, and again, it would take for only one of these users to have his credentials stolen or brute forced to bring down your site.

Once the attacker logs in, he can input javascript code into any text-based fields, doing things like redirecting users to malicious sites, steal passwords, or collect data about your site and users.

If you use frontend forms and let unauthenticated users fill them out
In this case, you need to make sure that you escape the output everywhere, be it front or backend.

Now that we’ve cleared this out – onwards to the solution!

ACF 4 vs ACF 5 / ACF PRO

In ACF 4, you can escape text and textfield fields by using get_field() and setting “Formatting: No Formatting” when configuring the fields. In the upcoming ACF 5 (and ACF PRO), this has been removed and replace with a format_value filter. Unfortunately, this filter does not let you target an individual field. In that case you have to escape data on the frontend.

Securing Advanced Custom Fields

Luckily, it’s easy to do escaping on the front end with built-in WordPress escaping functions.

I’ve written a handly helper function called get_field_escaped() which you can use instead of get_field() to secure your output.

It takes the same parameters as get_field(), so you can do stuff like:

echo get_field_escaped('my_custom_field');

You can also pass $post_id and $format_value, just like in the regular get_field() function:

echo get_field_escaped('my_custom_field', $post_id, true);

What’s new, a new fourth parameter has been added. It selects which escaping method is used on the data before it is sent back. By default is uses esc_html, which is an all-round function for escaping HTML.

You can easily switch this for any other escaping function that WordPress has, for example here we escape a URL:

<a href="<?php echo get_field_escaped('url', $post_id, true, 'esc_url'); ?>">My link</a>

And here we are escape a HTML attribute:

<div class="<?php get_field_escaped('custom_css_class', $post_id, true, 'esc_attr'); ?>">My div</div>

You can also pass NULL as the fourth parameter instead of the function name to skip the escaping. In that case it works exactly as get_field():

echo get_field_escaped('my_field', $post_id, true, NULL);

And the function itself:

/**
 * Helper function to get escaped field from ACF
 * and also normalize values.
 *
 * @param $field_key
 * @param bool $post_id
 * @param bool $format_value
 * @param string $escape_method esc_html / esc_attr or NULL for none
 * @return array|bool|string
 */
function get_field_escaped($field_key, $post_id = false, $format_value = true, $escape_method = 'esc_html')
{
    $field = get_field($field_key, $post_id, $format_value);

    /* Check for null and falsy values and always return space */
    if($field === NULL || $field === FALSE)
        $field = '';

    /* Handle arrays */
    if(is_array($field))
    {
        $field_escaped = array();
        foreach($field as $key => $value)
        {
            $field_escaped[$key] = ($escape_method === NULL) ? $value : $escape_method($value);
        }
        return $field_escaped;
    }
    else
        return ($escape_method === NULL) ? $field : $escape_method($field);
}

While we’re at it, let’s add the_field_escaped() as well in case we want to print the value directly.

/**
 * Wrapper function for get_field_escaped() that echoes the value isntead of returning it.
 *
 * @param $field_key
 * @param bool $post_id
 * @param bool $format_value
 * @param string $escape_method esc_html / esc_attr or NULL for none
 */
function the_field_escaped($field_key, $post_id = false, $format_value = true, $escape_method = 'esc_html')
{
    //Get field
    $value = get_field_escaped($field_key, $post_id, $format_value, $escape_method );

    //Print arrays as comma-separated strings, as per get_field() behaviour.
    if( is_array($value) )
    {
        $value = @implode( ', ', $value );
    }

    //Echo result
    echo $value;
}

That lets us do:

//Prints the value
the_field_escaped('my_field', $post_id, true, NULL);

XSS self-test

If you want to perform an XSS self-test, you can enter the following code in all your ACF text fields:

<script>alert('pwned')</script>

Then, visit the frontend on your site. If you see something similar to this message, you are susceptible to XSS:

xss

Other resources

Validating, Sanitizing, and Escaping, from WordPress VIP

Full-stack impostor syndrome sufferer & Software Engineer at Schibsted Media Group

Comments 21
  • Monika T-S
    Posted on

    Monika T-S Monika T-S

    Reply Author

    Hi Stanislaw
    I found your blog while searching for the author of “Email Obfuscate Shortcode” => .. my curiosity was piqued :-)

    Your function makes the task a lot easier.

    Thanks a lot!
    Monika


  • Marco
    Posted on

    Marco Marco

    Reply Author

    Hey Stanislav

    Does this also apply to the_field and the_sub_field in cases using repeater fields? Or can we leave these as they are?

    Many thanks!


    • Stanislav Khromov
      Posted on

      Stanislav Khromov Stanislav Khromov

      Reply Author

      Hey Marco,

      the_field() is just a wrapper for get_field(), which we can see in the ACF source code:
      https://github.com/elliotcondon/acf/blob/686eba9290c7133f492fbc3bc318e4c50804a2a5/core/api.php#L331

      the_sub_field() is a wrapper for get_sub_field(), which is a bit more complex.

      Generally, I would suggest always using get_sub_field(), and add appropriate escaping, for example:

      echo esc_html(get_sub_field($sub_field_name));
      

      You can also write a wrapper similar to get_field_escaped().

      Hope that helps!


      • David Hedley
        Posted on

        David Hedley David Hedley

        Reply Author

        Hi Stanislav,

        Do you happen to have an example wrapper for the_field_escaped() by any chance?

        Thanks in advance!


        • Stanislav Khromov
          Posted on

          Stanislav Khromov Stanislav Khromov

          Reply Author

          Hey David,

          I added the code for the_field_escaped() at the end of the post. Let me know if you have any troubles with it. Cheers!


  • Nick
    Posted on

    Nick Nick

    Reply Author

    Just a note above your sample has a typo
    alert(‘pwned’) should be alert(‘pwned’)


    • Nick
      Posted on

      Nick Nick

      Reply Author

      well that didn’t work! you have /alert instead of /script


      • Stanislav Khromov
        Posted on

        Stanislav Khromov Stanislav Khromov

        Reply Author

        Haha, that’s quite an embarrassing mistake. Thanks for the correction Nick! :-)


  • Jenny Beaumont
    Posted on

    Jenny Beaumont Jenny Beaumont

    Reply Author

    Fantastic post, thank you! Have a question – I often use a simple text field for URLs because I want a way to input a “nice” address for display, to which I’ll add the HTTP:// to the link. In this case, what’s the most secure way to escape this whole link? With part of it being manually coded, the other part retrieved from a custom field?
    Many thanks in advance !
    -jennyb


    • Stanislav Khromov
      Posted on

      Stanislav Khromov Stanislav Khromov

      Reply Author

      Hey Jenny,

      You can escape URL:s with the esc_url() function after you have finished gluing it together, ie:


      echo esc_url('http://'. get_field('your_url_field'));

      Or, if you want to use the helper function I wrote for this post, you can use the following code:


      echo 'http://' . get_field_escaped('your_url_field', false, true, 'esc_url');

      You probably also want to check if the user added http:// or https:// himself in the field and add/remove your “http://” string accordingly so you don’t get it twice by mistake. Many browsers now include the http(s):// part when you copy and paste a URL from the address bar.


  • Davide
    Posted on

    Davide Davide

    Reply Author

    Should update this post, Elliot now allows sanitization of the code by adding this filter. I tested it and it works great. This should remove a lot of headaches at all :)

    http://www.advancedcustomfields.com/resources/acf_form/#security


    • Stanislav Khromov
      Posted on

      Stanislav Khromov Stanislav Khromov

      Reply Author

      That code example gives basic support for removing dangerous HTML from all fields as they are saved.

      There are multiple issues with this approach.

      For one, it mangles your data as it makes its way into the database. Sometimes that means the user inputs value A but gets something else (B) in return. The preferable way is to escape data as it is being output instead.

      Second, wp_kses() is not suitable as a “catch-all” solution. Some fields are output in places like HTML attributes or inline JavaScript. This is why WordPress has a bunch of context-specific escaping functions like esc_html, esc_js, esc_attr and so on. So why not use the “right” functions for the job? :-)

      Interestingly, the format_value filter has been updated in a recent version of ACF PRO to let you target it by field, for example:
      acf/format_value/key=KEY, so now you can at least use that filtering approach instead of escaping in the templates.


  • Stefano Monteiro
    Posted on

    Stefano Monteiro Stefano Monteiro

    Reply Author

    Is this recommended for input fields located in the admin as well?


    • Stanislav Khromov
      Posted on

      Stanislav Khromov Stanislav Khromov

      Reply Author

      If you are using the field values in metaboxes or other places in the WordPress admin, you should escape the output there as well, as they suffer from the same potential security risks.


  • Jon
    Posted on

    Jon Jon

    Reply Author

    Thanks for this guide it works great!
    Is there a way to exclude simple tags or when using this in text area fields? I have found that it is not adding the paragraphs in my text area boxes that the user adds.
    Thanks
    Jon


    • Stanislav Khromov
      Posted on

      Stanislav Khromov Stanislav Khromov

      Reply Author

      Hey Jon!

      Lack of function to escape multiline data isa known problem in core, there is an issue about it with some suggestions on how to work around it:
      https://core.trac.wordpress.org/ticket/46188

      You can either create your own escape function (the author in the post above suggests `esc_br_html`).

      You can also try just wrapping the output from `esc_html` in `wpautop`, which will reintroduce line break tags. Something like this:

      echo wpautop(get_field_escaped(‘my_field’, $post_id, true);


  • Nabeel Haider
    Posted on

    Nabeel Haider Nabeel Haider

    Reply Author

    Here you go, this is a complete overhaul of your code. With all possibilities included that contains any sub field in field, any repeating values or any other scenario.

    /**
    * Helper function to get escaped field from ACF
    * and also normalize values.
    *
    * @param $field_key
    * @param bool $post_id
    * @param bool $format_value
    * @param string $escape_method esc_html / esc_attr or NULL for none
    * @return array|bool|string
    */
    function get_fields_escaped($field_key, $escape_method = ‘esc_html’)
    {
    $field = get_fields($field_key);

    /* Check for null and falsy values and always return space */
    if($field === NULL || $field === FALSE)
    $field = ”;

    /* Handle arrays */
    if(is_array($field))
    {
    $field_escaped = array();
    foreach($field as $key => $value)
    {
    if(is_array($value)){
    $field_escaped[$key] = get_sub_field_escaped($value, $escape_method);
    } else {
    $field_escaped[$key] = ($escape_method === NULL) ? $value : $escape_method($value);
    // $field_escaped[$key] = esc_html($value);
    }
    }
    return $field_escaped;
    }
    else
    return ($escape_method === NULL) ? $field : $escape_method($field);
    }

    function get_sub_field_escaped($parent =null, $escape_method = ‘esc_html’ )
    {
    $field = $parent;

    /* Check for null and falsy values and always return space */
    if($field === NULL || $field === FALSE)
    $field = ”;

    /* Handle arrays */
    if(is_array($field))
    {
    $field_escaped = array();
    foreach($field as $key => $value)
    {
    if(is_array($value)){
    var_dump($value);
    $field_escaped[$key] = get_sub_field_escaped($value, $escape_method);
    } else {
    // $field_escaped[$key] = esc_html($value);
    $field_escaped[$key] = ($escape_method === NULL) ? $value : $escape_method($value);
    }
    }
    return $field_escaped;
    }
    else
    return ($escape_method === NULL) ? $field : $escape_method($field);
    }

    // Here are some examples

    // Call the values
    $fields = get_fields_escaped ( $pID );

    // Use the values
    $eware_thho_hello_bar = $fields[‘eware_thho_hello_bar’];
    $eware_thho_hello_bar_visibility = $eware_thho_hello_bar[‘visibility’];
    $eware_thho_hello_bar_first_logo = $eware_thho_hello_bar[‘first_logo’];
    $eware_thho_hello_bar_second_logo = $eware_thho_hello_bar[‘second_logo’];
    $eware_thho_hello_bar_text = $eware_thho_hello_bar[‘text’];
    $eware_thho_hello_bar_button = $eware_thho_hello_bar[‘button’];
    $eware_thho_title = $fields[‘eware_thho_title’];
    if(!$eware_thho_title){
    $eware_thho_title = get_the_title();
    }