Skip to content

bobby_dreamer

Cross-Site Request Forgery (CSRF) and CSURF module

firebase, csrf, nodejs, javascript, time-wasted5 min read

Bottom-line. Wasted a week(4/Jan/2021 - 9/Jan/2021) on Cross-Site Request Forgery (CSRF). It all started with me working on Firebase server-side authentication and in the example i was following, it had CSRF setup. So, i started investigating and studying CSRF and found it interesting wanted to try it out. So, i'd setup firebase hosting, csurf and finally got it work for all GET requests. But, when i introduced POST requests, it started failing with invalid csrf token errors. Did a lot of attempts with solutions posted in S.O, nothing worked. So finally, i came across a post in S.O and decided CSRF is not required for firebase hosting. I proceeded to try out csurf without using firebase hosting and finally gave up.

Hmmm... Curiosity kills time.

This is how it all started.

# So, what is CSRF ?

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.

# Prevention measures that do NOT work

  • Using a secret cookie
  • Only accepting POST requests
  • Multi-Step Transactions
  • URL Rewriting
  • HTTPS

# What is csurf ?

csurf is a middleware which creates token via req.csrfToken() and this token will be generated and added to all requests and makes sure that the request comes from a legitimate client. This generated token can be added as a Cookie or via templating in meta tags or form hidden fields whichever suits your needs. Here, i am storing it in a cookie, so additionally i am using cookie-parser, another middleware. Why this choice, if you ask, basically i am using firebase and i am developing this based on examples from Firebase : Managing Cookies.

How CSRF works ? When the server sends a form/page to the client, it attaches a unique random value (the CSRF token) to it that the client needs to send back. When the server receives the request from the client, it compares the received token value with the previously generated value. If they match, it assumes that the request is valid.

Here are the steps to implement, this has nothing to do with actual firebase, here we are just makingt the site more secure.

  1. Install both the modules npm install cookie-parser csurf --save
  2. In the index.js add the below code in the middleware section. In line 5, we are just stating we will be using cookie object to store the secret for the user. Other notable default value is name of the cookie will be _csrf
1...
2const cookieParser = require("cookie-parser");
3const csrf = require("csurf"); // protect from Cross site forgery attacks
4...
5app.use(cookieParser());
6const csrfMiddleware = csrf({ cookie: true });
7app.use(csrfMiddleware);
  1. Here, line 4 gets executed before executing the specific route code and we are setting the cookie XSRF_TOKEN. In our example, just triggered the route /login from the browser.
1/** Routes
2**********************************************************/
3//This executes first and set the cookie to XSRF-TOKEN
4app.all("*", (req, res, next) => {
5 const XSRF_TOKEN = req.csrfToken();
6 console.log(`In (*), XSRF_TOKEN=${XSRF_TOKEN}`);
7 res.cookie("XSRF-TOKEN", XSRF_TOKEN);
8 next();
9});
10
11app.get('/login', function(req, res){
12 console.log(`In ${req.path}, req.cookies=${JSON.stringify(req.cookies)}`);
13 res.clearCookie('__session');
14 res.render('login.ejs', { title: "login - Sushanth Tests", pageID: "login"})
15});

CSURF1

  1. You can see the same XSRF_TOKEN and _crsf cookies in the browser. CSURF2

  2. In the browser, when i enter the email and password and click login following code in firebase gets triggered. Here i am doing following things,

  • In firebase onAuthStateChanged() method will be triggered whenever state of the user is changed. So, when the state changes from logged off to logged in, we can generate ID token via getIdToken() method.
  • We retreive the cookie and post it back via headers and firebase ID Token in the body.
  • We are signing out because, we don't need to be logged in the client side any more as we have generated the ID Token using which we can retreive the UserID for further processing from the server side.
1// Check AUTH state change
2 firebase.auth().onAuthStateChanged(firebaseUser => {
3 if(firebaseUser){
4 setUserDetails(firebaseUser).then( (results) => {
5 firebaseUser.getIdToken().then(function(idToken) {
6 console.log(idToken); // It shows the Firebase token now
7
8 const csrfToken = Cookies.get("XSRF-TOKEN");
9 console.log("Cookie Get : ",csrfToken);
10
11 return fetch("/sessionLogin", {
12 method: "POST",
13 headers: {
14 Accept: "application/json",
15 "Content-Type": "application/json",
16 "CSRF-Token": Cookies.get("XSRF-TOKEN"),
17 },
18 body: JSON.stringify({ idToken }),
19 });
20 }).then( response => {
21 firebase.auth().signOut();
22 if(!response.ok){
23 console.log(response);
24 throw new Error(response.status+", "+response.statusText);
25 }else{
26 window.location.assign("/");
27 }
28 }).catch( error => {
29 window.location.assign("/login");
30 });
31
32 }).catch( (error) => {
33
34 console.log('Status : ',error);
35 setTimeout( () => {
36 const auth = firebase.auth();
37 auth.signOut();
38 }, 5000);
39 });
40
41 }
42 });
  1. In the server, when the post route /sessionLogin is called by fetch below code gets triggered. Before executing the code in the route, csurf validates the tokens automatically and once its successful only, route code is executed. In the first part of the code, i am just displaying the idToken, req.cookies, req.headers and req.body and in the second part of the code, we are create a firebase session cookie with the name __session. This is special because firebase hosting, functions and cloud run permits only this cookie to pass through to the execution of your app and rest are all stripped from incoming requests. This is mentioned in Manage cache behavior.
1//Login Setup and Check
2app.post("/sessionLogin", (req, res) => {
3 const idToken = req.body.idToken.toString();
4 console.log(`In ${req.path}, idToken=${idToken}`);
5 console.log(`In ${req.path}, req.cookies=${JSON.stringify(req.cookies)}=`);
6 console.log(`In ${req.path}, req.headers=${JSON.stringify(req.headers)}=`);
7 // console.log(`In ${req.path}, req.body=${JSON.stringify(req.body)}=`);
8
9 const expiresIn = 60 * 60 * 24 * 5 * 1000;
10
11 admin.auth().createSessionCookie(idToken, { expiresIn }).then( (sessionCookie) => {
12 console.log(`In ${req.path} : And in createSessionCookie()` );
13 const options = { maxAge: expiresIn, httpOnly: true };
14 res.cookie("__session", sessionCookie, options);
15 res.end(JSON.stringify({ status: "success" }));
16 }, (error) => {
17 res.status(403).send("UNAUTHORIZED REQUEST!");
18 }
19 );
20});

When the route /sessionLogin is executed, we get the following output,

1i functions: Beginning execution of "app"
2> In (*), XSRF_TOKEN=c8xlEuv3-I3Dc_dseJdNCgqLwVdy-ctrJ5r8
3> In /sessionLogin, idToken=eyJhbGciOiJSUzI1NiIsImtpZCI6ImUwOGI0NzM0YjYxNmE0...............VQ-g75TTw
4> In /sessionLogin, req.cookies={"_csrf":"_FKcfg_uQ3UCd8x2ct1iQp5c","XSRF-TOKEN":"KNneQQhf-H1roUFVXyP-80qUZk9IUqwyUdtM"}=
5> In /sessionLogin, req.headers={"x-forwarded-host":"localhost:5000","x-original-url":"/sessionLogin","pragma":"no-cache","cache-control":"no-cache, no-store","host":"localhost:5001","connection":"keep-alive","content-length":"930","sec-ch-ua":"\"Google Chrome\";v=\"87\", \" Not;A Brand\";v=\"99\", \"Chromium\";v=\"87\"","accept":"application/json","csrf-token":"KNneQQhf-H1roUFVXyP-80qUZk9IUqwyUdtM","sec-ch-ua-mobile":"?0","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36","content-type":"application/json","origin":"http://localhost:5000","sec-fetch-site":"same-origin","sec-fetch-mode":"cors","sec-fetch-dest":"empty","referer":"http://localhost:5000/login","accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","cookie":"_csrf=_FKcfg_uQ3UCd8x2ct1iQp5c; XSRF-TOKEN=KNneQQhf-H1roUFVXyP-80qUZk9IUqwyUdtM"}=
  1. Let logout and test the csrf tokens. Below is the logout route and do note, if you don't clear the cookie __session, you sort of get invalid csrf token errors. So i am clearing that cookie in signout and login(it took two days to figure that out).
1app.get('/signout', async function(req, res){
2 var sessionCookie = req.cookies.__session;
3 console.log(`In ${req.path}, sessionCookie=${sessionCookie}`);
4
5 let fbUser = await admin.auth().verifySessionCookie(sessionCookie, true/** checkRevoked */);
6 console.log(`In ${req.path}, uid=${fbUser.uid}, email=${fbUser.email}, auth_time=${fbUser.auth_time}`);
7
8 res.clearCookie('__session');
9 res.redirect('/login');
10});

Below are the steps, i am performing.

  1. Click signout

  2. Go to login page and clear storage CSURF3

  3. Enter the login route http://localhost:5000/login again to you will get the new tokens now. CSURF4

  4. Entered email, password and manually updated the XSRF-TOKEN. Just added an extra s. CSURF5

  5. Click SIGN-IN WITH EMAIL

  6. Below are the responses,

  • In the server
1[hosting] Rewriting /sessionLogin to http://localhost:5001/api-project-333122123186/us-central1/app for local Function app
2i functions: Beginning execution of "app"
3> EBADCSRFTOKEN invalid csrf token
4i functions: Finished "app" in ~1s
5i hosting: 127.0.0.1 - - [06/Jan/2021:07:30:20 +0000] "POST /sessionLogin HTTP/1.1" 403 42 "http://localhost:5000/login" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
  • At the client CSURF6

You will be similar responses, if _csrf token is changed as well.

Custom error handling

Two things are handled here,

  1. Bad csrf token
  2. Checking request origin
1app.use(function (err, req, res, next) {
2 // res.clearCookie('__session'); //It is found that it is found that removing __session from cookie resolves
3 // the EBADCSRFTOKEN error which kept reoccuring during testing 2 days wasted. After removing this __session cookie
4 // was able to proceed.
5 console.log(err.code, err.message);
6 if (err.code !== 'EBADCSRFTOKEN'){
7
8 //Checking the request origin
9 const referer = (req.headers.referer? new URL(req.headers.referer).host : req.headers.host);
10 const origin = (req.headers.origin? new URL(req.headers.origin).host : null);
11 console.log("Orgin Checks");
12 console.log(`req.headers.host=${req.headers.host}`);
13 console.log(`req.headers.referer=${req.headers.referer}, ${new URL(req.headers.referer).host}`);
14 console.log(`req.headers.origin=${req.headers.origin}, ${new URL(req.headers.origin).host}`);
15
16 if (req.headers.host == (origin || referer)) {
17 next();
18 } else {
19 return next(new Error('Unallowed origin'));
20 }
21 // return next(err);
22 }
23
24 // handle CSRF token errors here
25 res.status(403).send({message:"CSURF code has been tampered"});
26});

Everything was fine till here and when i introduced POST requests, started getting invalid csrf token and after sometime, i found this S.O response from bojeil

CSRF becomes an issue when you are saving a session cookie. Firebase Auth currently persists the Auth State in web storage (localStorage/indexedDB) and are not transmitted along the requests. You are expected to run client side code to get the Firebase ID token and pass it along the request via header, or POST body, etc. On your backend, you would verify the ID token before serving restricted content or processing authenticated requests. This is why in its current form, CSRF is not a problem since Javascript is needed to get the ID token from local storage and local storage is single host origin making it not accessible from different origins.

If you plan to save the ID token in a cookie or set your own session cookie after Firebase Authentication, you should then look into guarding against CSRF attacks.

bojeil response to another S.O question on same subject

getCookie is a basically a cookie getter. You can write it yourself or lookup the implementation online. As for the CSRF check, this is a basic defense against CSRF attacks. The CSRF token is set in a cookie and then returned back in the post body. The backend will confirm that the CSRF token in the cookie matches the token in the POST body. Basically the idea here is that only requests coming from your website can read the cookie and pass it in the request in the POST body. If the request is coming from another website, they will not be able to read the cookie and pass it in the POST body. While the CSRF token cookie will be always be passed along the request even when coming from other origins, the token will not be available in the POST body.

It's unique per protocol://host:port combination.

After reading this, i decided not go with CSRF and Firebase combination but wanted to try out csurf in a regular ExpressJS app and i was getting the same error.

And at this point, i quit with csurf. Thinking about what it does,

  1. SERVER : You make a get request and csurf generates a unique token
  2. SERVER : You set the token in a cookie and call it _csrf or XSRF-TOKEN
  3. SERVER : You also render the same token to the webpage by passing it in a input hidden field in the form (or) pass it to a meta tag.
  4. CLIENT : When button is clicked which could be a submit or post, you pass the CSRF token via HEADER or BODY
  5. SERVER : You retreive the token in the cookie and token which is passed in header (or) body and compare it.
  6. SERVER : if they are same, you process the request otherwise fail it as 401 Unauthorized request.

Attempted to do the above without using csurf, just to have a little satisfaction that i didn't waste a entire week on this.

index.js
1...
2function attachCsrfToken(url, cookie, value) {
3 return function(req, res, next) {
4 console.log(`in attachCsrfToken(), cookie=${cookie}, value=${value}, req.url=${req.url}, url=${url}`);
5 res.clearCookie(cookie);
6 res.cookie(cookie, value);
7 app.set(cookie, value);
8 /*
9 if (req.url == url) {
10 res.cookie(cookie, value);
11 }
12 */
13 next();
14 }
15}
16
17...
18/** Middleware
19**********************************************************/
20...
21app.use(cookieParser());
22
23// Attach CSRF token on each request
24// - Funny thing about this Math.random() most of the time its the same value in a session
25app.use(attachCsrfToken('/', 'csrfToken', (Math.random()* 100000000000000000).toString()));
26
27app.post('/sessionLogin', function(req, res){
28 console.log(`In ${req.path}`);
29 const email = req.body.email.toString();
30 const password = req.body.pass.toString();
31 console.log(`In ${req.path}, ${email}, ${password}`);
32
33 //If cookies.csrfToken not found, it will set as -1 otherwise it gets the value from cookie
34 const csrfToken = !req.cookies.csrfToken ? -1 : req.body.csrfToken.toString();
35
36 // Guard against CSRF attacks.
37 if (!req.cookies || csrfToken !== req.cookies.csrfToken) {
38 console.log(`In ${req.path}, csrfToken=${csrfToken}, req.cookies.csrfToken=${req.cookies.csrfToken}`);
39 res.status(401).send('UNAUTHORIZED REQUEST!');
40 return;
41 }
42
43 // Set session expiration to 5 days.
44 const expiresIn = 60 * 60 * 24 * 5 * 1000;
45 const options = { maxAge: expiresIn, httpOnly: true };
46
47 if(email=="abc@xyz.com" && password=="123789"){
48 res.cookie("userpass", "authorized", options);
49
50 res.writeHead(200, {"Content-Type": "application/json"});
51 res.end(JSON.stringify({ ok:true, status:200,message:'Success' }));
52 // res.redirect("/");
53 }else{
54 res.cookie("userpass", "unauthorized", options);
55
56 res.writeHead(401, {"Content-Type": "application/json"});
57 res.end(JSON.stringify({ ok:false, status:401,message:'Unauthorized' }));
58 }
59});
60
61// - home
62app.get('/', isAuthenticated, function(req, res){
63 console.log(`In ${req.path}`);
64 // console.log(`In ${req.path}, ${JSON.stringify(req.headers)}`);
65 var csrfToken = req.app.get('csrfToken');
66 res.render('home.ejs', { title: "home", csrfToken})
67});
login.ejs
1<meta name="csrf-token" content="<%= csrfToken %>">

Client side login.js script

login.js
1document.addEventListener('DOMContentLoaded', event => {
2
3 /**** Elements
4 **********************************************************/
5 const txtEmail = document.getElementById('txtEmail');
6 const txtPassword = document.getElementById('txtPassword');
7 const btnLogin = document.getElementById('btnLogin');
8
9 // Read the CSRF token from the <meta> tag
10 var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
11
12 /**** Event Listeners
13 **********************************************************/
14 btnLogin.addEventListener('click', async (e) => {
15 let email = txtEmail.value;
16 let pass = txtPassword.value;
17 console.log(token);
18
19 let url = '/sessionLogin';
20 let options = {
21 method: 'POST',
22 url: url,
23 headers: { 'Accept': 'application/json', 'Content-Type': 'application/json;charset=UTF-8' },
24 data: {email, pass, csrfToken: token }
25 };
26
27 await axios(options).then((response) => {
28 // Success
29 if(response.status==200 && response.statusText=="OK"){
30 console.log(response.config);
31 console.log(response.data);
32 window.location = '/';
33 }else{
34 console.log(response);
35 }
36 }).catch((error) => {
37 // Error
38 console.log("Error=",error);
39 if (error.response) {
40 // The request was made and the server responded with a status code
41 // that falls out of the range of 2xx
42 // console.log(error.response.data);
43 // console.log(error.response.status);
44 // console.log(error.response.headers);
45 } else if (error.request) {
46 // The request was made but no response was received
47 // `error.request` is an instance of XMLHttpRequest in the
48 // browser and an instance of
49 // http.ClientRequest in node.js
50 console.log(error.request);
51 } else {
52 // Something happened in setting up the request that triggered an Error
53 console.log('Error', error.message);
54 }
55 console.log("Error Config=",error.config);
56 });
57
58 });
59
60});

# References