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.
Good addition Rickard and team.
Hope to hear more about 1.3 in the near future.
October 2nd, 2007, at 5:01 pm #neat solution. look forward to 1.3!
October 2nd, 2007, at 6:39 pm #This sounds like a good solution to the problem. Good to finally hear some news about 1.3 as well, since it’s been awfully quiet around here for a while! :-)
An idea I got for improving this “csrf_token” is to actually store it in the database (in the user table, for example), and then change it every time it’s used. That way, a given user can only do a POST once with a given token, and when he does, the token is changed in the database and in the form’s hidden field, so the last token is made invalid.
Since PunBB is the one creating the token, it’s impossible for an attacker to know the next token, but the current one can be retreived from XSS attacks. That won’t matter much, though, since the attacker won’t get his hands on the user’s credentials, which are also required for the POST to be successful.
Since the token will be changed very often, a good algorithm to generate it would be UUID (or GUID). An SHA1 or MD5 of a completely random string would work as well, of course. As long as it’s impossible to predict what the next token is going to be, this isn’t very important.
October 3rd, 2007, at 7:29 am #Asbjørn: Good suggestion.
October 4th, 2007, at 2:09 am #Just to clarify though, the CSRF token is stored in the database already, in the online table.
While regenerating the token after every POSTed request is a good idea, it does have some downsides. For example, if I have the forum open in more than one window, I can’t submit forms from both windows (or rather, I can’t submit a form in one and then submit a form in another: I would need to reload the second form before submitting it). I think the current solution does a good job of balancing usability with security; whenever your row is deleted from the online table, either by logging out or by a timeout, when you login again you get a new token.
The currently implemented solution definately increases security, but I don’t think my proposed solution would cause much of an issue in real life; I at least don’t open several forms in separate tabs or windows and post them all simultaneously. People are different, though, so I understand your current approach.
October 9th, 2007, at 8:40 am #I frequently delete multiple posts by opening their delete forms in multiple tabs rather than going to moderate posts :P
October 9th, 2007, at 10:55 pm #I think the better way is combine online.csfr_token and page address to generate and check referrer. Something like md5($scrf_token.$_SERVER['SCRIPT_NAME'])
So, the scrf_token will NEVER shown, malicious user cannot guess token for any page but script can confirm http referrer!
I’ve implement this in my development forum.
November 4th, 2007, at 6:15 am #artoodetoo: There’s a couple problems with that
November 6th, 2007, at 1:36 am #1. It means that the referrer is once again required, which is what we’re trying to avoid.
2. $_SERVER[’SCRIPT_NAME’] does not accurately reflect the referrer, especially with the new SEF URLs.
Neal, as I said before I have working engine with such technique.
SCRIPT_NAME is always stable with or without SEF URLs. So we continue checking with old-style pattern like this:
confirm_referrer(’profile.php’);
instead of SEF-version
confirm_referrer(’user/’)
or something like this.
It’s another vote for this technique: legacy code with confirm_referrer() will work.
Source: http://punbb-pe.org.ru/viewtopic.php?pid=277#p277
November 6th, 2007, at 8:25 am #Where does that md5 hash get used?
November 6th, 2007, at 8:50 am #artoodetoo: aha, I see what you’re saying. I was confused, I thought you were advocating using the HTTP_REFERER to generate part of the hash to compare against.
November 8th, 2007, at 3:23 am #It’s not a bad idea, although I think the gains in security are minimal. It also means that the referrer check function needs to be called properly by every script, whereas the current solution protects all POSTed data.
“…the current solution protects all POSTed data” Don’t put all your eggs in one basket. :)
If ANY (public) form has XSS-vulnerability, then easy to catch admin’s token and post data to ANY OTHER (admin’s-only) form.
My aproach, imho, is workaround both the token and the http referer limits.
November 8th, 2007, at 9:17 am #artoodetoo: I’ll see what I can do ;)
November 9th, 2007, at 1:02 pm #I’m actually not going to do that, because as I said the burden is then on the developer to write a check in the proper place. So, any form where people forget to write it won’t be protected at all.
Instead, I’m thinking about re-generating the token after every successful request.
Edit: And of course, I forgot that I had this debate with Asbjørn a couple comments ago. Lucky for me, someone pointed it out. :P
November 10th, 2007, at 2:24 pm #So yeah, I’m not exactly sure what can be done to improve it without sacrificing usability or putting the burden of security on people writing the forms. We’re thinking about it though ;)
(One more try, I hope the moderator deletes the messed up posts)
Humor me, what about this instead:
form method=”post” action=”somescript.php”
input type=”hidden” name=”csrf_token” value=”php echo sha1($some_secret_salt . $_SERVER['SCRIPT_NAME'] . time)”
input type=”hidden” name=”script_name” value=”php echo $_SERVER['SCRIPT_NAME']”
input type=”hidden” name=”time” value=”php echo time”
/form
The script_name and time can’t be tampered with because the hacker does not know the secret salt. The salt could be generated a number of ways; it can be per user or system wide, it can be permanent or regenerated for each logout/login. Plus, this way the token is always different, and there is no multiple browser problem.
December 28th, 2007, at 6:48 am #I just deleted the two other posts ;)
December 28th, 2007, at 5:11 pm #That’s good, except I think that would still have the issue of a CSRF token from one page being good for posting to any other page. We would, after all, want to allow one page to have a form on it that posts to another.
Thanks for taking care of that and for your response :)
I don’t mean comparing the hashed/hidden script_name to the form-processor’s script_name. Hash the user’s secret token from the database with the time and the script_name, then also include the time and script_name plaintext in the form. The form processor then checks that the hash is correct using the secret token from the database and the values from the form, and also uses the time from the hidden form field to make sure the form isn’t too old, and uses the script_name from the hidden form field to make sure it is something that is expected–like a referrer check without having to use HTTP_REFERER. This way, you can’t use the same token from (for example) the “post a topic” form for the “change your password” form. With the time, you can make individual forms expire after a set time. With this method the “public token” is always different but posting from multiple browser windows is not a problem.
Anyway, I am just exploring possibilities and giving you ideas; not trying to persuade you one way or the other. I liked artoodetoo’s method but this method has all the advantages and none of the disadvantages as his.
December 28th, 2007, at 10:37 pm #I understood what you meant ;)
However, as I said, it would still open you up to a token being useful for any form: just set all the variables as they were. The problem is that we don’t want to restrict a page to only submitting to itself or to a fixed set of pages. That would solve the problem rather easily using your solution. However, it would be somewhat difficult for extension developers and we don’t want use of the token to become unnecessarily complicated. The basic idea is good and sound though. ;)
Rickard brought up a way to deal with this that is somewhat similar to yours and artoodetoo’s. The idea is that the hash should be composed of a random string and the page to which the form is going to be posted to (not the page posting the form). That way, given the base hash, any PunBB page can generate a hash to submit to any other page. However, the base hash won’t ever be exposed to the user, so you couldn’t just use the hash for submitting to any form.
December 28th, 2007, at 11:58 pm #Rickard’s idea sounds great … not that I am surprised he came up with a good idea, he came up with PunBB after all ;)
Anyway, I appreciate the discussion here and seeing what’s on the minds of the developers. Good luck with 1.3 :)
December 29th, 2007, at 6:48 am #I just committed the new system to the repository if anyone is interested in taking a look and commenting. :)
January 5th, 2008, at 11:09 pm #Neal & Rickard, good job!
January 12th, 2008, at 2:16 pm #