Preventing CSRF attacks

Users of PunBB 1.2 and earlier versions are familiar with the infamous referrer check. The referrer check, although fully functional, has caused many administrators and moderators quite a lot of grief. It’s confusing and intrusive.

What the referrer check does is to make sure that any GET or POST data that is submitted to PunBB actually originates from one of PunBB’s scripts. It does this by comparing the value of the HTTP_REFERER header (misspelled in the spec) to the expected value (e.g. http://punbb.org/forums/delete.php). The HTTP_REFERER header is sent by all browsers when it is referred from one page to another and contains the URL of the referring page. Why does this matter? Well, consider this scenario.

An administrator of a PunBB forum clicks a link to an external page posted by a malicious user. On this external page, the malicious user has constructed a hidden form that automatically submits when anyone visits the page. The form submits to the profile.php script of the administrators PunBB install with the parameter update_group_membership, the user ID of the malicious user and the destination group - administrators. The administrator doesn’t even noticed that the form submits in the background and boom, the hacker has managed to escalate himself to administrator. He then logs in, deletes everything and posts his usual “h4cked by whatever” everywhere. Not good.

The above is one variation of what is commonly called a cross-site request forgery, or just CSRF (pronounced “sea surf”). The referrer check prevents this kind of attack because when the administrator visits the external page and the form gets submitted to profile.php, PunBB detects that it was submitted from an external page and spits out a warning/error message.

I have, over the years, received quite a few questions regarding the referrer check. Lots of people are uncertain about its efficiency considering the ease at which HTTP_REFERER can be spoofed. Spoofing of HTTP_REFERER is a non-issue though. A hacker can spoof his own HTTP_REFERER but we don’t care about that, we’re interested in the HTTP_REFERER of the forum administrator when he submits a form. This can’t, as far as I know, be modified externally.

Spoofing aside, the most common issue that users have with the referrer check is that for some users, it just won’t work properly. The reason for this usually is that they have some kind of “Internet security” software suite (I put that in quotes on purpose) installed that strips out HTTP_REFERER from all browser requests. Other users have encountered problems with proxies that strip out or alter HTTP_REFERER. The bottom line is that there are lots of ways in which the referrer check can fail, and we can’t have that.

In PunBB 1.3, the referrer check is no more. To replace it, we’ve implemented an anti-CSRF token system. Fancy words for something relatively simple. Here’s how it works.

All users browsing a PunBB 1.3 forum are assigned a temporary random SHA1 hash that we refer to as the CSRF token. The token is only valid for one session. As soon as the user times out or logs out, the token is destroyed. Now, armed with the token, whenever we present a form to an administrator or moderator, we include the token in a hidden field in that form. Here’s an example:

<form method="post" action="somescript.php">
    <input type="hidden" name="csrf_token" value="7fa7097b4dc1f3bc22895cd95e2183cbc165ece0" />
    the regular form fields...
</form>

When the form is submitted and somescript.php receives the form data, we compare the value of the hidden form field to the token we have stored for the user in question. If there’s a mismatch or if the token is missing, we display a message stating that there was a token mismatch.

This system prevents CSRF attacks as effectively as the referrer check, but it is much less prone to issues out of our control. The only real issue with the token system is that the token has a certain time to live. If an administrator or moderator navigates to a form in which the csrf_token field is included and then waits for an hour or so before he submits the form, he will be assigned a new token when the script that receives the form data loads and there will be a mismatch. It’s not the end of the world though. Hitting the back button, refreshing the page and then submitting again will solve the problem.

At this moment, the CSRF token system is only used for administrators and moderators, but in the future, we might enable its use for other user groups as well.