Script removal or deleted HTML in WordPress Multisite post content

The title is a little misleading but my guess is this is what WordPress developers will be searching for so, to help other developers who stumble across this bug, by the way, it is not a bug it’s about permissions, here goes!

There is a function called map_meta_cap in /wp-includes/capabilities.php – this takes 3 parameters

function map_meta_cap( $cap, $user_id, $args ) {
...
}

$cap for the capability like ‘edit_post’, ‘delete_post’, ‘remove_user’, ‘add_user’ etc, $user_id to identify the user and $args for additional parameters as an array. This function is massive in fact and spans something like 600 lines of code because there are lots and lots of capabilities in WordPress. Buried in this function is a capability called ‘unfiltered_html’.

If you look at the code for this capability in the function you’ll see there are 2 conditions that check against either a defined constant or if the site ‘is_multisite’ and the user is not a Super Admin with ‘! is_super_admin’ – I’ll come on to the latter later, let’s look at what’s going on first.

if ( defined( 'DISALLOW_UNFILTERED_HTML' ) && DISALLOW_UNFILTERED_HTML ) {
   $caps[] = 'do_not_allow';
} elseif ( is_multisite() && ! is_super_admin( $user_id ) ) {
   $caps[] = 'do_not_allow';
} else {
   $caps[] = 'unfiltered_html';
}

So why is this? Well, in a standard WordPress install unfiltered HTML allows the user to add HTML elements to the content as well as other places and that in turn gets directly saved to the database, this is probably the most common entry point for any third-party hack, if a hacker can get into your site they could easily add a <script> tag that injects some nasty JavaScript and in turn do all sort of nasty things!

But, in most cases the user will want to add some elements of HTML which are ok for most standard WordPress websites, one website hacked is a little easier to fix than hundreds that could be hosted in a Multisite!

Of course, if you wanted to disallow users from adding HTML all together you could add the following to your wp-config.php file which would not allow any HTML to be added anywhere.

# Disallow unfiltered_html for all users, even admins and super admins
define( 'DISALLOW_UNFILTERED_HTML', true );

So now let’s look at the second condition:

...
} elseif ( is_multisite() && ! is_super_admin( $user_id ) ) {
   $caps[] = 'do_not_allow';
...

Now, this is the bit that threw me! I manage a few Multisites but last week was the first time I came across this issue.

This conditions simply checks if it is multisite and if the user is not a Super Admin, who is someone who has overall access to the network of WordPress multisite’s, if the user was an Administrator or Editor the $caps[] array would get ‘do_not_allow’ added to it, thus preventing any user other than a Super Admin from adding any HTML to the particular site they are currently logged in to.

In some cases, this would be fine as you wouldn’t want one site hacked in a Multisite that could potentially affect all sites across the network! However, in my case the Administrator needed to add some training materials that were embedded in a <iframe> tag, although it looked like they could add an HTML block and pasted in their <iframe> code, this function would simply strip out the code and they could never save the materials needed.

Fortunately, you can override this function as it’s returned through a filter…

return apply_filters( 'map_meta_cap', $caps, $cap, $user_id, $args );

To do this simply you can create your own function and hook it to the filter ‘map_meta_cap’ like so…

function unfilter_html_for_admin( $caps, $cap, $user_id ) {
   if ( 'unfiltered_html' === $cap && user_can( $user_id, 'administrator' ) ) {
      $caps = array( 'unfiltered_html' );
   }
   return $caps;
}
add_filter( 'map_meta_cap', 'unfilter_html_for_admin', 1, 3 );

Be aware of what you are doing here though and make sure you trust the users that need to add HTML to their content when using a Multisite setup. You could, of course, take it a step further by only allowing certain users to add HTML by checking their ID first like so…

function unfilter_html_for_user( $caps, $cap, $user_id ) {
   if ( 'unfiltered_html' === $cap && $user_id === 123 ) {
      $caps = array( 'unfiltered_html' );
   }
   return $caps;
}
add_filter( 'map_meta_cap', 'unfilter_html_for_user', 1, 3 );

You could also do it by the specific site using the ‘get_current_blog_id()’ to identify the website you want to allow unfiltered HTML like so…

function unfilter_html_by_site_id( $caps, $cap, $user_id ) {
   $site_id = get_current_blog_id();
   if ( 'unfiltered_html' === $cap && $site_id === 123 ) {
      $caps = array( 'unfiltered_html' );
   }
   return $caps;
}
add_filter( 'map_meta_cap', 'unfilter_html_by_site_id', 1, 3 );

There you have it, I had a little fun with this last week and I hope it will help others.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *