In this blog post, I will discuss how the SRCF recently hardened the control panel against cross-site request forgery (CSRF) attacks. These attacks allow malicious sites to perform actions in the control panel on your behalf.
To understand the attack, let us first consider how authentication works with
the control panel (and most websites out there). After the control panel is
convinced that you are who you are, it sets a cookie in the browser.
Afterwards, whenever you visit any page on
control.srcf.net, the browser
sends the cookie back to the web server alongside with the request. The web
server then compares the cookie with what it has in its records to know who you
authenticated as. If the cookie is invalid or not present, it sends you back to
the authentication page.
Crucially, this communication is only between the browser and the webserver.
The browser will send the cookies only to
control.srcf.net and no one else.
usually prohibited by setting the
HttpOnly attribute (this prevents
attacks). A webpage can never read cookies on a different domain.
Note: A user is able to read their own cookies and send requests with arbitrary cookies. Cookies are usually cryptographically signed by the webserver to ensure they were indeed set by the webserver. We are not protecting against the user here. After all, the user already has the authority to make changes on the control panel. We are preventing malicious sites from making requests on the user’s behalf.
A user wants to reset their MySQL password. In the control panel, they would encounter a form that looks like
<form action="/member/mysql/password" method="post"> <input type="submit" value="Confirm"> </form>
Once they press the “Confirm” button, the browser submits the form by sending a
POST request to
https://control.srcf.net/member/mysql/password. The request
will contain the authentication cookies previously set, which is used to verify
How to perform the attack
Suppose our user has previously logged into the control panel to do some work, and later visits a malicious site. The site wants to reset the user’s MySQL password. What the attacker can do is to make an identical form on their website:
<form action="https://control.srcf.net/member/mysql/password" method="post"> <input type="submit" value="Confirm"> </form>
If they somehow trick the user into clicking the “Confirm” button, then this
submits a request to the control panel as if the user submitted it from the
control panel. Since we are accessing
https://control.srcf.net/ directly via
the browser, the browser sends the authentication cookies with the request.
This does not require the attacker to learn the contents of the cookie —
they instead trick the browser into sending it for them.
<!-- exploit.html --> <!DOCTYPE HTML> <html> <body> <form action="https://control.srcf.net/member/mysql/password" method="post"> <input type="submit" value="Confirm"> </form> <script> document.forms.submit() </script> </body> </html>
This will successfully trick the browser into resetting the MySQL password, but the user will know it after the fact, since they get redirected to the control panel. To hide this from the user, we can put this in an iframe:
<!-- better_exploit.html --> <!DOCTYPE HTML> <html> <body> <iframe style="display: none" src="exploit.html"></iframe> </body> </html>
By directing users to
better_exploit.html, the exploit in
in the iframe which is hidden from the user, and the user would not notice anything.
Attacks that don’t work
Before we discuss mitigations to this attack, we first look at some versions of the attack that don’t work. This is pretty important — if these worked, the attacker can combine these with the previous attack to bypass our CSRF protections.
If an attacker wants to retrieve password-protected information, they can try to embed the control panel into their webpage via an iframe. However, browsers do not allow sites to access contents of an iframe if it comes from a separate domain. We can also set headers to tell the browser to not allow embedding
control.srcf.netinto anyone’s iframe, which prevents this problem for good.
To protect users against CSRF attacks, we use a strategy called “double-submit cookie pattern”.
When the user logs into the control panel, we place a random string in the
cookie, which we call the
csrf_token. In every form the user accesses, we
csrf_token as a hidden field:
<form action="/member/mysql/password" method="post"> <input type="hidden" name="csrf_token" value="totally-a-secret"> <input type="submit" value="Confirm"> </form>
When the user submits a request, we obtain the
csrf_token from two sources
— one in the
Cookie header and one in the form data. We then check that
If a malicious party attempts to carry out a CSRF attack, they can trick the
browser into sending the
csrf_token in the
Cookie header. However, they
never get to learn the value of the
csrf_token, so they cannot include the
token in their form data when they submit on the user’s behalf.
Naively, the attacker might try to steal the token by accessing the form itself
control.srcf.net, which contains the
csrf_token. However, as mentioned,
the attacker cannot read the contents of pages on
redirect people to it, so this also doesn’t work.
Fortunately for us, there are already libraries that handle all this for us.
which makes CSRF protection a one-liner (after adding the hidden fields to all
of our forms, which is a simple
- We use resetting of MySQL passwords as an example, because it is a relatively non-disruptive thing to test on. The attack works for any action; for more complex requests, we need a pre-filled form instead of one with just a submit button.
- Our mitigations assume that browsers are reasonably compliant and secure. This is a necessary assumption. After all, a broken browser might allow malicious sites to access and modify all your cookies on all sites, or even delete all files on your filesystem.
- A reader was concerned about our use of
sed; it might miss something. However, this is not a security concern. If we forget to add the
csrf_tokensomewhere, the request will just be always rejected for not having a matching token in the form data. Not the best for someone who actually wants to carry out the action, but we will most likely notice this immediately and deploy a fix.