Cross-Site Request Forgery (CSRF) Prevention In A Node.js Backend Application
Learn how to prevent CSRF attacks in Node.js with the use of CSRF tokens
What is Cross-Site Request Forgery (CSRF)?
Cross-Site Request Forgery (CSRF) is an attack where the attacker tricks the users of a trusted site into executing dangerous and unwanted actions on the web application in which they’re currently authenticated/logged in, usually with a little help of social engineering (such as sending a link via email or chat). A successful CSRF attack can end up performing undesired functions by sending state changing requests on the victim’s behalf like fund transfers, changing email addresses of victims, and so on. At worst case scenario, an entire web application can be compromised if this attack is successfully done on an admin account.
In effect, CSRF attacks are used by an attacker to make a target system perform a function via the victim's browser, without the victim's knowledge, at least until the unauthorized transaction has been committed.
CSRF uses an attack vector that specifically attacks requests where the browser automatically provides authentication (typically cookies), and that means that it is usually done on users with active logged in sessions on the application. A CSRF attack works because browser requests automatically include all cookies including session cookies.
How Does It work?
There many ways to orchestrate this form of attack, one of the more popular example is to trick users into submitting a form or loading a maliciously crafted URL link with social engineering. This URL may be used to send a request to send an amount of money from a victim's account to the attackers account. or to change a user's email address to the attacker's email address. Yes, you may lose your money or login access to a trusted site through a CSRF attack.
The social engineering aspect may be to send an email with html contents to the victim or to plant deadly scripts on webpages that a victim is likely to visit.
Prevention
The possibility of performing CSRF attacks makes it important to add an extra layer of authentication in an application. All that had to be done before is to authenticate users by asking them to login before performing tasks, but we now have to authenticate each request.
One of the methods of thwarting this kind of attack is using CSRF tokens. So, how do CSRF tokens work?
- Server sends the client a token.
- Client submits a request with the token.
- The server rejects the request if the token is invalid or unavailable.
CSRF tokens can prevent CSRF attacks by making it impossible for an attacker to construct a fully valid HTTP request suitable for feeding to a victim user. Since the attacker cannot determine or predict the value of a user’s CSRF token, they cannot construct a request with all the parameters that are necessary for the application to honor the request. Fortunately, there is a Node.js CSRF protection middleware, the Express team's csrf and csurf modules.
Using CSRF Tokens WIth Express
This is a summarized line-by-line walkthrough of the whole process of using the CSURF module,
Initialize a new node.js project with
npm init -y
Install the following npm packages to start the project. You can search for what each package does for a clearer picture.
npm install express esm csurf cookie-parser body-parser nodemon morgan cors
In the package.json file, add the start script for the project as follows, (the use of
esm
(es modules) is optional).
"scripts": {
"start": "nodemon -r esm index.js"
},
- Create an
index.js
file in the root folder and start to import the needed modules in there.
import express from "express"; //express app
import cors from "cors"; // for handling cross-site request headers
import csrf from "csurf"; // for CSRF token creation and validation
import cookieParser from "cookie-parser"; // csurf requires a cookie or session middleware
import bodyParser from "body-parser" // request body handler
const morgan = require("morgan"); // (OPTIONAL) HTTP request logger middleware for node.js
Create an express app and setup route middlewares
var csrfProtection = csrf({ cookie: true }) var parseForm = bodyParser.urlencoded({ extended: false }) // create express app const app = express()
Apply express middlewares
app.use( cors({ origin: "http://localhost:3000", // request origin, set this to the url is expected to originate from methods: "GET,HEAD,PUT,PATCH,POST,DELETE", // allowed request methods allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-CSRF-Token", }) ); app.use(cookieParser()); app.use(morgan("dev")); app.use(csrfProtection);
Setup test routes and controllers
// route to serve CSRF Tokens
app.get("/api/csrf-token", (req, res) => {
res.json({ csrfToken: req.csrfToken() });
});
// sample PUT request route for testing purposes
app.put("/api/data", (req, res) => {
res.json({ ok: true });
});
- Listen for requests at your chosen port (8000 in this case) and start server with
npm start
// port
const port = 8000;
app.listen(port, () => console.log(`Server is running on port ${port}`));
Whenever a PUT request is made to the /api/data
route, the HTTP headers is automatically checked for any value that includes X-XSRF-Token
, X-CSRF-Token
, XSRF-Token
or CSRF-Token
. This is then validated on the backend, the request will be allowed or rejected based on the comparison result.
Implementing A Client Using React and axios to send requests.
A typical react app or any frontend application should send all state changing requests with the generated CSRF tokens and all session cookies received from the backend application.
import { useState, useEffect } from "react";
import axios from "axios";
import "./App.css";
function App() {
const [email, setEmail] = useState("you@gmail.com");
// function to get CSRF Token from backend
const getCsrfToken = async () => {
const { data } = await axios.get("http://localhost:8000/api/csrf-token");
// log data received
console.log("CSRF", data);
// set default header with axios
axios.defaults.headers["X-CSRF-TOKEN"] = data.csrfToken;
};
// send request on page load
useEffect(() => {
getCsrfToken();
}, []);
// send post request
const handleSubmit = async (e) => {
e.preventDefault();
try {
const { data } = await axios.put(
"http://localhost:8000/api/data",
{
email,
},
{ withCredentials: true }
);
console.log(data);
} catch (err) {
console.log(err.response.data);
}
};
// jsx
return (
<div className="App">
<header className="App-header">
<form onSubmit={handleSubmit}>
<input
type="email"
className="input"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
required
/>
<button type="submit" className="button">
Submit
</button>
</form>
</header>
</div>
);
}
export default App;
Conclusion
It is worth looking into for anyone working with cookie-based authentication to implement a CSRF defence technique to prevent malicious individuals from sending dangerous state changing requests. Modern browsers are increasingly beneficial at thwarting a lot of malicious attempts from attackers but developers should not be too lax at implementing custom security protocols because you can not be too secure. For more information on CSRF and other prevention methods, the referenced links below will be of immense help.
References
- [OWASP Cross-Site Request Forgery Prevention Cheat Sheet] (cheatsheetseries.owasp.org/cheatsheets/Cros..)
- OWASP Cross Site Request Forgery (CSRF) Attack Page
- Understanding CSRF
- Introduction to CSRF: How can a cookie get you hacked?