WordPress doesn’t use a nonce for the login form, which opens up for you to perform a WordPress session donation attack.

What is a session donation attack?

I trick you to click a link. If I know you, I can simply give it to you. If not I can use some sort of phishing or similar. On the web page, in a hidden iframe offscreen (or similar), I will autosubmit a form to log you into my own account. A cookie will be set in your browser, so the next time you go to the WordPress site, you will automatically be logged into my account.

Why would I want you to be logged in as me? And how is that considered an attack?

Let’s say the site in question is a paid membership site. My subscription have ended, and I need to pay to renew it. I know exacly how the suspended account email looks like (since I got one when my subscription ended), so I can easily phish you into clicking it. The link is even 100% legitimate, as it actually does go to the correct membership site.

To make sure you don’t suspect anything is wrong, I’ll update my profile to use your name, and set my display name to your first name. WordPress will then greet you with “Howdy, Yourname”.

Then you update my credit card details using your credit card details.

Thank you.

Session donation attack in WordPress

The WordPress login page sets the HTTP-header X-Frame-Options: SAMEORIGIN, which restricts the browser from loading (or a rather important distinction: display) the WordPress login form from a different domain in a frame, including iframes. Also, the cross origin policy restricts me from interacting with a page from a different orgin, i.e. domain.

But I can (almost) simply provide the form myself. WordPress doesn’t check the referrer or anything like that. WordPress doesn’t care where the POST-request is originating from. But WordPress does one check: Upon loading the login form, WordPress sets a wordpress_test_cookie session cookie, which is checked for during the authorization, simply to check if the browser accepts cookies so the user can be logged in.

This means that the session donation attack will only be successful if the victim have already visited the WordPress login form during their current browser session. To be honest to ourselves: It’s a fairly low chance of that. So unless we can manage to set the cookie, we can’t perform the attack. Setting the cookie, means we have to get the victim to somehow visit the WordPress login page.

Circling back to the X-Frame-Options: SAMEORIGIN which restricts the browser for displaying the page in a frame. Well, as it turns out, it doesn’t discard the request completly. All headers – including the cookie headers – are parsed as normal. Including storing the set cookies. This means we can just include the login form in a hidden iframe to have the cookie set, and secretly post the form to that (or another) hidden iframe.

The final step would be to redirect you directly to the page where you fill in your credit card details for me.

Here’s the complete code for a working proof of concept:

https://gist.github.com/bjornjohansen/f780c250ba2f1c712abdaea4a5781e2c

How to protect your WordPress site from session donation attacks

To protect your WordPress site, you must make sure that the login process can’t be done automatically, but make sure the form really is interactive.

Add a CAPTCHA to the login

Adding a CAPTCHA to the login stop these attacks, as they are interactive in nature. As long as the user logging in has to interact with it, it should be fine. Google has a new CAPTCHA solution that doesn’t necessarily requires user interaction. Solutions like that, which are interaction-free, will not help against session donation attacks.

Use two-factor authentication

Some two-factor authentication solutions will also help. Note that the important part here is that they force the login form to be interactive, e.g. by entering one time passwords. Two-factor authentication solutions that automatically sends the user’s phone  a push notification that they have to confirm on the phone, will still enable the attack. The attacker just have to be ready to confirm whenever the attack is underway. The important thing is that we force the user to interact with the login form, e.g. by having to enter a code or asking for a push notification to their device.

Will adding a nonce to the login form help?

WordPress nonces are not real nonces, as they can be reused within a certain timeframe, and they are based on the logged in users’s user-id. Implementing IP-based nonces (instead of the userid-based nonces WordPress currently use) would not be effective if the attacker and victim is on the same network (the attacker could just do a request to the login form to pull out the nonce field). If something like nonces should be used, we have to make sure they are really unique for the visitor or that they are not “number used once”, but rather “number issued once”.

The long-term solution: Same-Site Cookies

When issuing the test cookie, the SameSite attribute should be set. When the browser sees that attribute, it will not send the cookie along with requests that doesn’t originate from the same site. Thus, the proof-of-concept above will not work, as it requires the test cookie to be sent along.

Same-Site cookies have two modes: Strict mode and Lax mode.

Strict mode will prevent the cookie being sent along with any request at all that doesn’t originate from the same site. This includes regular plain old links. Strict mode is probably a little too strict as a default for this, as it means users will have to relogin every time they visit a link to the site.

Lax mode will pass along the cookie with regular links, but refuse to pass it along with POST requests – which is exactly what we need to block to stop session donation attacks.

Same-Site cookies blocks all CSRF attacks

Same-Site cookies won’t just help mitigate our session donation attack, but will also block all other Cross-Site Request Forgery attacks, as it will be impossible to forge requests across sites. Neat, huh?

Scott Helme has an excellent introduction to Same-Site cookies in the article Cross-Site Request Forgery is dead!

When will WordPress use Same-Site Cookies?

Core committer John Blackbourn opened Trac ticket #37000 Support for SameSite cookie attribute in February 2016. For now, it still needs some dev-feedback and a patch.

There is a patch for PHP7 that updates setcookie() to allow for the SameSite attribute to be set, but even with that, WordPress needs to support PHP versions all the way back to PHP5, and thus will have to rely on a separate library. The PHP library PHP-Cookie has a requirement for PHP 5.6, so if using that as a base, it will have to be forked and made compatible with PHP 5.2 first.

If you have the Lua module for Nginx, or use OpenResty, then you can do so, yes.

I also think it is possible to do it with the ngx_headers_more module, but I haven’t dug into that yet.

If you know how this can be done in a fairly standard LEMP stack assumed to be widespread, please share so in the comments!

Does the WordPress security team know about this?

Yes, they do. If they didn’t before, I emailed them January 25, 2017 where I also provided a live, working proof-of-concept. I’ve yet to receive an answer, but through a back-channel I know that they have received it.

This is not a serious vulnerability to WordPress in general, but it can be serious to a few people that rely on the correct individual being logged in. Nasty people who are capable of exploiting this, probably already know about the concept of session attacks already, and WordPress is so widespread I can’t believe it’s not a target. It is important to warn those who might be at risk, so they can do something to mitigate it.

When publishing this, it’s been 8 weeks since I contacted the WordPress security team. They know I have this blog post, and I’ve not been told to not publish it, so I can only assume they have drawn the same conclusion regarding the low severity of this.