How does WP-ZEP prevent zero-day attack?

Posted on May 11, 2021

IP Location Block is the only plugin which has an ability to prevent zero-day attack even if some of the plugins in a WordPress site have unveiled vulnerability. I call it “Zero-day Exploit Prevention for WordPress” (WP-ZEP).

In this article, I’ll explain its mechanism and also its limitations. Before that, I’ll mention the best practice of plugin actions.

Overview

There are three methods to prevent zero-day attacks. The first one is to inspect bad queries with the blacklisted signatures. And the second one is to filter out only the legitimate requests using the whitelist. Those methods are focused on the patterns of request.

The last one, which WP-ZEP adopts, is to close a hole of vulnerability in a certain way that is focused on the patterns of vulnerability.
A considerable number of vulnerable plugins and themes that were out there are lacking invalidating either the nonce and privilege or both. So WP-ZEP will make up both of them embedding a nonce into the link, form, and ajax request from jQuery on every admin screen.

What’s the best practice of plugin actions?

While we can find the answer at Stack Exchange, I’d like to describe it a little in detail.

Showing plugin page

An url to the plugin dashboard can be specified depending on its parent category as follows:

  • wp-admin/admin.php?page=my-plugin
  • wp-admin/tools.php?page=my-plugin
  • wp-admin/option-general.php?page=my-plugin

Requesting to wp-admin/admin.php

On the plugin dashboard, we can provide an action do-my-action via admin.php with a form like this:

<?php add_action( 'admin_action_' . 'do-my-action', 'my_action' ); ?>
<form action="<?php echo admin_url( 'admin.php' ); ?>">
    <?php wp_nonce_field( 'do-my-action' ); ?>
    <input type="hidden" name="action" value="do-my-action" />
    <input type="submit" class="button" value="Do my action" />
</form>

Or a link:

<?php
$link = add_query_arg(
    array(
        'action' => 'do-my-action',
        '_wpnonce' => wp_create_nonce( 'do-my-action' ),
    ),
    admin_url( 'admin.php' )
);
?>
<a href="<?php echo esc_url( $link ); ?>">Do my action</a>

Requesting to wp-admin/admin-ajax.php

We can also do the same thing via admin-ajax.php by GET or POST method using jQuery. This request can be handled by the wp_ajax_xxxx action hook.

<?php add_action( 'wp_ajax_' . 'do-my-action', 'my_action' ); ?>

Requesting to wp-admin/admin-post.php

WordPress also gives us a chance to handle POST request via admin-post.php.

<?php add_action( 'admin_post_' . 'do-my-action', 'my_action' ); ?>

Processing the requests

All above-mentioned can be processed by the function my_action().

<?php
function my_action() {
    // validate privilege and nonce
    if ( ! current_user_can( 'manage_options' ) ||
         ! check_admin_referer( 'do-my-action' ) ) {
        return; // force to redirect to login page
    }

    // do my action
    $result = ...;

    // show result in case of Ajax
    if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
        wp_send_json( $result );
    }

    // show result after page transition
    else {
        if ( isset( $_REQUEST['_wp_http_referer'] ) ) {
            // redirect to the referer by wp_nonce_field()
            $redirect_to = $_REQUEST['_wp_http_referer'];
        }
        else if ( isset( $_SERVER['HTTP_REFERER'] ) ) {
            // redirect to the referer by browser
            $redirect_to = $_SERVER['HTTP_REFERER'];
        }
        else {
            // redirect to the plugin page
            $redirect_to = admin_url( 'admin.php?page=my-plugin' );
        }

        wp_safe_redirect( $redirect_to );
    }

    die();
}
?>

The mechanism of WP-ZEP

In my_action(), the most important processes before doing my action are:

  1. validate user privilege with current_user_can().
  2. validate the nonce with check_admin_referer().
  3. validate the given input.

When a plugin developer loses one of those, the result becomes serious.

So WP-ZEP will make up 1. and 2. by embedding a nonce into the request.

The limitations of WP-ZEP

One big challenge for WP-ZEP is to embed a nonce. You already notice that there’re countlessly many ways to do their own job besides the best practice. For example, one can do the “job” without using “action”.

wp-admin/?page=my-plugin&job=do-my-job

In this case, the requested “job” will be processed directly in their hander without any help from WordPress core. So WP-ZEP can do nothing about it.

Another big challenge is to decide whether the request handler is vulnerable or not it my_action() is registered for both authorized and unauthorized users like this:

<?php
add_action( 'wp_ajax_'        . 'do-my-action', 'my_action' );
add_action( 'wp_ajax_nopriv_' . 'do-my-action', 'my_action' );
?>

If WP-ZEP blocks the action do-my-action, visitors on the public-facing pages can not get any services via the ajax call. So WP-ZEP should carefully identify the action only for the admin. If the action provides its services for both admin and visitors, all this plugin has to do is validating an IP address by its county code.

This causes a serious problem: vulnerability in Slider Revolution cannot be blocked when the attack comes from the permitted country. (Because it had added the above two actions 😠). To prevent this kind of attack, you should add the following snippet into your functions.php to filter out the malicious signatures. (This functionality had already been implemented from version 0.2.2.0.)

<?php
add_filter( 'ip-location-block-admin', 'my_protectives' );
function my_protectives( $validate ) {
    $signatures = array(
        'wp-config.php',
        'passwd',
    );

    $req = strtolower( urldecode( serialize( $_GET + $_POST ) ) );

    foreach ( $signatures as $item ) {
        if ( strpos( $req, $item ) !== FALSE ) {
            $validate['result'] = 'blocked';
            break;
        }
    }

    return $validate;
}
?>

The last limitation is related to the validation of user privilege. WP-ZEP can not know which privilege is needed to do-my-action. For example, some plugins need manage_options, while moderate_comments is sufficient for others. So all WP-ZEP can do is to validate if a user is logged in or not as a minimum privilege.

This limitation will tolerate the vulnerability of Privilege Escalation. You should prohibit guest users from registrating to pretend it.

Conclusion

So many security plugins are there on WordPress.org, but nothing is perfect against the “Zero-day Attack”.

So I’d like to keep this plugin simple and light enough to collaborate with other plugins while playing a certain degree of role by itself 👻.

Leave the first comment