Looking for a similar platform? We’d love to hear from you!

  Take a Survey
Tutorials, WordPress

3 Essential Rules For WordPress Code Security

by Maciej Rozbicki

WordPress code security should never be overlooked, but this doesn’t mean it isn’t. While plenty of other factors can affect you or your client’s website safety, introducing simple rules to your code can go a long way.

WordPress may seem a fantastic solution to create your website or offer it to your client. Thanks to its robust plugin and theme environment, WordPress’s capabilities extend far beyond a simple blogging system. But, just like superheroes, developers must remember that with great power comes great responsibility (yes, I know this quote is overused, but it’s true).

Ensuring that your code is secure is no easy feat, and web application security is a topic of pretty much infinite complexity. The amount of plugin vulnerabilities popping up in databases every day is terrifying. So unless you have some superpowers up your sleeve, here are three elementary guidelines that should help you stay on the right track!

WordPress code security is a matter that shouldn't be overlooked.

Don’t Trust User Input

When it comes to processing user input, being paranoid is the only valid strategy. Always assume the worst.

All the HTML and JS running on the client’s side can be altered. Expect to get values different from the ones you define in your dropdowns, checkboxes, or radios. Don’t be surprised when you receive an array instead of a string. What makes matters worse is that hackers can inject code both in your $_GET and $_POST variables. SQL injection is the #1 threat on the OWASP Top Ten list, and XSS (Cross-Site-Scripting) is #7. Both are caused by improper user input handling.

SQL injection happens when you include user input in your SQL queries without proper escaping. Avoid adding it to your queries whenever you can. For example, when you need to include some string in the query based on the users’s choice, but the range of input is known and limited, you could use a ternary operator or an array with a whitelist or any other means that will allow you to get the user input out of the equation.

$order_by = $_GET['order-by'] === 'asc' ? 'asc' : 'desc';

$categories = ['fruits', 'vegetables', 'meat'];
$category = in_array($_GET['category'], $categories, true) ? $_GET['category'] : 'all';

When all you need to get from the user is an integer, cast the variable to an integer.

$id = (int) $_GET['id'];
// ...

if (isset($_GET['show-product'])) {
    global $wpdb;
    $results = $wpdb->get_results(
        'SELECT * FROM ' . $wpdb->prefix . 'posts WHERE post_type = "product" AND ID =' 
        . $_GET['show-product'], // SQL Injection opportunity, not sanitized input from $_GET
        ARRAY_A
    );

    foreach ($results as $result) {
        $content .= '<div class="product">'
            . '<div class="product__title">' . $result['post_title'] . '</div>'
            . '<div class="product__content">' . $result['post_content'] . '</div>'
            . '</div>';
    }

    return $content;
}

In the following code we’d of course be better off using WordPress post functions, but let’s see what could happen if we were to skip input sanitization:

Now, if we replace the ID in the URL by adding an always true condition OR 1=1 like so:

http://example.com/?show-product=7%20OR%201=1

it will print out not only product posts, but all the WordPress posts. Not the end of the world, right? Wrong! Using tools like sqlmap, you can dump entire database through blind SQL injection.

When you need to include user input in your query, use a parametrized one, but make sure to do preparation only once, as double preparation isn’t secure at all.

// ...

$query = $wpdb->prepare(
    'SELECT * FROM ' . $wpdb->prefix . 'posts WHERE post_type = "product" AND ID = %d',
    $_GET['show-product']
);

$results = $wpdb->get_results(
    $query,
    ARRAY_A
);

// ...

That being said, keep in mind that there were issues with $wpdb in the past. Connecting to the database directly brings its own set of headaches, so think twice before deciding to go for it.

In an XSS attack, hackers aim to execute their JS code in other clients’ browsers. If you think this isn’t game over yet, think again. If the browser belongs to the administrator, hackers can take advantage of it and send a request to WordPress to create a new admin account. The possibilities are endless, user data can leak in more subtle ways and you won’t even notice. XSS vulnerability arises when attackers are able to inject a <script> tag or other tag allowing to pass JS code that will execute like <img onerror="...">. The best solution is to escape HTML entities altogether and allow users to format their text in some other way if necessary.

echo '<input type="text" name="some-search-field" value="'
    . ($_GET['search-query'] ?? '') // XSS opportunity - not sanitized $_GET input passed to value
    .  '">';

The above code is an example of Reflected XSS vulnerability. To perform a Reflected XSS, hackers prepare a malicious link that they send to other users of the website. This simply means that they embed JS in GET argument like so:

http://example.com/?search-query=%22%3E%3Cscript%3Ealert(%60hey%60)%3C/script%3E

When an unsuspecting user clicks the link above, the embedded code gets echoed out as HTML and is executed. This method requires user interaction and may not be very efficient, but you still need to remember it can happen.

Stored XSS works the same way as Reflected XSS, with the difference that it doesn’t require user interaction as the malicious code is stored on the server and sent to visitors with the rest of the content. This way, users (including admins) can get hacked just by visiting the site.

Don’t Trust Client-Side Code

One of the key WordPress security code issues is forgetting that all the code and HTML running in the client’s browser can be altered. Form validation on the front-end is purely a UX feature and offers no security benefits. Another thing to remember is that when something isn’t visible to the user on the front-end, it absolutely doesn’t mean that it’s not reachable on the backend. Consider the following, commonly seen WordPress code:

// Front-end JS
function createProduct(name, description) {
    var data = {
        '_ajax_nonce': ajax_object.nonce,
        'action': 'create_product',
        'product_name': name,
        'product_description': description,
    };

    return jQuery.post(ajax_object.ajax_url, data);
}
// Back-end PHP
function create_product() {
    check_ajax_referer('product_manager'); // reusable nonce
    
    // no input sanitization (persistent XSS opportunity)
    $new_product = array(
        'post_type'    => 'product',
        'post_title'   => $_POST['product_name'] ?? '',
        'post_content' => $_POST['product_description'] ?? '',
        'post_status'  => 'publish',
        'post_author'  => get_current_user_id(),
    );

    $new_post_id = wp_insert_post($new_product);

    $data = [
        'product_id' => $new_post_id
    ];

    wp_send_json_success($data, 'success');
}

function update_product() {
    check_ajax_referer('product_manager'); // reusable nonce

    // no input sanitization
    $new_data = array(
        'ID'           => $_POST['product_id'],
        'post_type'    => 'product',
        'post_title'   => $_POST['product_name'] ?? '',
        'post_content' => $_POST['product_description'] ?? '',
        'post_status'  => 'publish',
    );

    $post_id = wp_update_post($new_data, true);

    $data = [
        'product_id' => $post_id,
    ];

    wp_send_json_success($data, 'success');
}

See anything missing aside from input sanitization? There’s no access control on the back-end. Even if you don’t include the handler on the front-end, nothing stops the attackers from editing the client-side code to their liking. So they could, for example, change it like this:

function createProduct(name, description) {
    var data = {
        '_ajax_nonce': ajax_object.nonce,
        'action': 'update_product',
        'product_id': 7,
        'product_name': 'Some new name',
        'product_description': 'Some new description',
    };

    return jQuery.post(ajax_object.ajax_url, data);
}

And the fact that WordPress nonces are not true nonces as stated in official documentation only makes things easier.

Don’t Assume Security by Obscurity

With the above example, you could argue that you’re writing some custom code that nobody’s ever going to look at, so who’s going to know the appropriate action name and parameters? But that’s a very poor approach to WordPress code security! You really shouldn’t hope nobody’s going to exploit any methods to get your code. Take the matter into your own hands and prevent this scenario from happening instead. Let’s consider the last example:

// Some encrypting function found on Stack Overflow, let's not focus on it's strength but on the way it's used
function xor_string($string, $key) {
    for($i = 0; $i < strlen($string); $i++) {
        $string[$i] = ($string[$i] ^ $key[$i % strlen($key)]);
    }

    return $string;
}

function create_product() {
    // ...

    // Notice that the encryption key is constant and hardcoded for all deployments
    // and ID's here are incremental and easy to iterate through
    $delete_key = base64_encode(xor_string(get_current_user_id() . '-' . $post_id . '-delete_post' , 'constant key'));

    $data = [
        'post_id'    => $post_id,
        'delete_key' => $delete_key
    ];

    wp_send_json_success($data, 'success');
}

function delete_product($delete_key) {
    $delete_key = xor_string(base64_decode($delete_key), 'constant key');
    $delete_key = explode('-', $delete_key, 3);
    
    $post_author = get_post_field('post_author', $delete_key[1])

    $deleted = false;
    if($delete_key[2] === 'delete_post' && $post_author === $delete_key[0]) {
        $deleted = wp_trash_post($delete_key[1]);
    }
}

Using constant and hardcoded key in combination with predictable components sounds like a bad idea straight away. Still, I came across a similar mistake in one of the immensely popular WordPress plugins. It’s super easy to write a piece of code that will iterate over user and post ID’s and generate links with GET argument.

Keeping Up With WordPress Code Security

Like I said at the beginning, WordPress website can be compromised in many different ways, some more obvious than others. Does this mean you get to freak out? Absolutely not! But are there measures that you can take against it? Sure thing!

Ensuring your code is sealed-shut from the beginning is one of the best things you can do to protect the website. It’ll save you many headaches and you’ll be able to switch your focus elsewhere. So always think about what you’re doing and try to abuse your own creation. You never know who might be looking 😉

Do you know other ways to seal your code shut? I'd love to hear your thoughts!

Maciej Rozbicki

Disqus Comments

Reading time 9 minutes

This website uses cookies to ensure you get the best experience. More info I agree