Protecting WordPress custom forms from CSRF attack

Franky Hung
Geek Culture
Published in
3 min readOct 18, 2021

--

You may have heard that WordPress is by far the most hacked CMS platform ever. Despite of that, many developers or bloggers worldwide are still using WordPress to build websites. That is why we should apply security measures wherever possible on WordPress sites.

CSRF attack has been around for a pretty long time. If your site isn’t protected against it, it is pretty easy for an attacker to hijack your session and perform state changes to your site without your consent. I suggest reading this article on OWASP to have a full understanding on CSRF attacks.

1. Crafting a CSRF attack against WordPress

Sometimes when form plugins cannot fulfil your customization needs, you will have to create your own custom forms, e.g. via inserting a custom shortcode. This form might be a login form, a contact form, or even something more complex that allows user to enter account details or make orders on your site. In the following example, I will create a simple change email form without any CSRF protection, that could be easily exploited by an attacker to trigger a POST request on a third-party malicious site to your site that changes your email to the attacker’s email.

Let’s say we implemented test-form as a shortcode that is being displayed somewhere on your site like this:

function test_form() {
ob_start();
?>
<h2>Change Email Form</h2>
<form method="POST" id="test-form">
<div class="input-group">
<label for="email" class="form-label">Your new email <span style="color: red">*</span></label>
<input type="email" name="new_email" id="email">
</div>
<button type="submit">Submit</button>
</form>
<?php
return ob_get_clean();
}
function test_form_shortcode() {
add_shortcode( 'test-form', __NAMESPACE__ . '\\test_form' );
}
add_action('init', __NAMESPACE__ . '\\test_form_shortcode');

Below that, you also added in your code to process the submission of said test-form :

function process_test_form() {
if (isset($_POST['new_email']) && is_user_logged_in()) {
// execute code to change user email to new email
...
wp_redirect( get_home_url() . '/change-email-success/' );
exit;
}
}
add_action( 'wp_loaded', __NAMESPACE__ . '\\process_test_form' );

Assuming a user of your site Tom, has been logged onto your site. He accidentally come across an exploited page that might look absolutely normal, but actually harbours a hidden iframe that points to the attacker’s malicious page which submits a POST request to your site on page load:

<body onload='document.CSRF.submit()'>
...
<form action="http://yoursite.com/change-email/" method="POST" name="CSRF">
<input type="hidden" name="new_email" id="email" value="hacker@example.com">
</form>
....
</body>

Tom wouldn’t notice a thing as that iframe is hidden, and at that time, Tom’s email address on your WordPress site has already been changed. Now the attacker might be able to go down the forgot password route on your site and hijacks Tom’s account. Later Tom will find out that he cannot log into his account anymore.

2. Patching up the CSRF vulnerability

Luckily, it is actually pretty easy to fix this. One easy way is to include a WordPress nonce in your form, and then verify the nonce every time you process the POST request.

Add this line at the top of your form:

<form id="test-form" method="POST">
<input name="form_nonce" type="hidden" value="<?=wp_create_nonce('test-nonce')?>" />
....

And then when you process your form, verify the nonce alongside with your original checking:

if (isset($_POST['form_nonce']) && wp_verify_nonce($_POST['form_nonce'],'test-nonce') && isset($_POST['new_email']) && is_user_logged_in()) {

After that, you’re already safe from most of the CSRF attacks out there. From now on, to complete the POST request, the attacker has to know the correct value of the nonce, you’re much safer than having no protection at all. However, this does not protect from replay attacks as the nonces generated by WordPress aren’t just used once. “Nor are they used only once, but have a limited “lifetime” after which they expire. During that time period, the same nonce will be generated for a given user in a given context.” — https://codex.wordpress.org/WordPress_Nonces

If you want your forms to have an extra layer of security, I suggest adding Google reCaptcha to your forms. Not only it protects your forms from CSRF attacks, it also protects you from spam bots.

--

--

Franky Hung
Geek Culture

Founder of Arkon Digital. I’m not a code fanatic, but I’m always amazed by what code can do. The endless possibilities in coding is what fascinates me everyday.