In the security world, CSRF, or cross-site request forgery, is one of the most problematic exploits to mitigate and stop. Not only are these attacks everywhere on the web, but their potential for damage is incalculable.
This article aims to serve as a starting point for JavaScript, TypeScript, and Node.js engineers in CSRF protection. We will briefly present what CSRF is, explore some examples of CSRF attacks, and finally provide some mitigation strategies against them.
It is important to remember that although this article focuses on Node.js development stacks and the examples are written in JavaScript, the basic strategies apply to any technology stack. You are not required to have mastery of Node.js or TypeScript. However, a basic understanding of JavaScript is required.
If you haven't dipped your toes in yet, please check out this site for more information.
Let's jump right in.
A Fresh Start With Node.js and TypeScript
OK, let's prepare our simple boilerplate project so that you can play around with the code and follow along with this post.
First, make sure to have Node.js installed. You can do this by running the following command in macOS:
curl "https://nodejs.org/dist/latest/node-${VERSION:-$(wget -qO- https://nodejs.org/dist/latest/ | sed -nE 's|.*>node-(.*)\.pkg</a>.*|\1|p')}.pkg" > "$HOME/Downloads/node-latest.pkg" && sudo installer -store -pkg "$HOME/Downloads/node-latest.pkg" -target "/"
Alternatively, you can use Homebrew to install it with the following command:
brew install nodejs
If you run any other system, you can find further instructions here.
Now, creating a Node.js project is extremely simple. Just create a folder called "nodejs_typescript_sample" (you can call this folder anything you'd like) and navigate to it. Once inside, create a file called "app.js" (we recommend you call it that) and paste the following code:
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});
Once you've done that, you can then go to the terminal and run the code using the following command:
node app.js
Visit http://localhost:3000, and you will see a message saying "Hello World."
Simple, right?
Clarifying CSRF
In simple terms, CSRF (also known as XSRF), as the name suggests, is an attack that relies on the user's privileges by hijacking their session to gain access to their data.
With this approach, an attacker circumvents the security of our platforms by deceiving the user into submitting a malicious request on their behalf.
CSRF attacks are possible because of two main things.
First, CSRF attacks exploit the user's inability to discern whether a legitimate HTML element contains malicious code.
Second, since these attacks come from legitimate users, the mechanisms we designed to protect them do not work. Thus, this allows bad actors to mislead users into sabotaging themselves.
Additionally, bad actors can mask HTML elements and use social engineering tactics through avenues such as chat or emails to a significant effect. And since these exploits seem to come from trusted origins for the average user, this hijacks their trust in the systems they rely on on a day-to-day basis.
Examples of CSRF Attacks
Now, let's explore how a CSRF attack can hijack a system with the following example.
A user receives an email from a seemingly trusted source. Say an attacker has emulated the format and look of a banking institution and has managed to mask the sender email to look legitimate enough.
Our victim, our non-tech-savvy aunt, sees the email, which conveys with a sense of urgency the need for her to click on a link provided to check on an unusual transaction that the bank has flagged as suspicious.
Our aunt then proceeds to hurriedly oblige and click on the link without verifying the authenticity of its origin. She is then sent to the bank website.
The website is legitimate and shows no sign of foul play. It even displays as secure, and the URL matches the website.
She then proceeds to either discard the email or call the bank.
Now, countless things could have happened here. For example, when our victim clicked on the malicious link, a targeted URL could have taken advantage of a new authenticated session (or something as simple as just having a tab open while logged in) and executed a change in the database to a vulnerable web application. The possibilities for harm are endless.
Keep in mind that there is much more down the rabbit hole of CSRF. Unfortunately, exploring the intricacies of how it exploits vulnerabilities in the protocols of the web goes outside of the scope of this article.
However, if you want to dive deeper into the ocean of CSRF and how it works, we recommend reading our sister article explaining it in more detail here.
How to Mitigate CSRF Vulnerabilities in Node.js and TypeScript
Now that we've seen how effortless it is to capture our user's sessions, let's explore some mitigating actions against CSRF attacks.
Routing Structure
The first thing we need to do is reconsider the design of the routing structure of our site.
CSRF attacks take advantage of systems that accept GET requests that perform a state change. This pattern is already a terrible practice, and you should avoid them in all scenarios.
Thankfully, addressing this is relatively straightforward in Express. All you need to do is change the route file (usually called "index.js") and set the application's action to receive a POST or PUT instead of a GET for state changes.
This change, of course, ensures that you follow the HTTP protocol guidelines.
//router.get('/posts/create', sessionController.validateSession, postsController.postsCreate);
router.post('/posts/create', sessionController.validateSession, postsController.postsCreate);
You will end up with something like the following:
Notice that all state-changing requests are not GET.
Keep in mind that this approach will not protect us from attacks from a form tag submitting a POST automatically by JavaScript.
Further, an attacker can adapt their exploits to work with JavaScript Ajax requests and submit any protocol or parameters necessary to accomplish their goal.
Action Confirmation
It is recommended that all developers implement confirmation mechanisms into all critical state-changing actions.
The implementation of a middle step between a submit, requiring the user to confirm their action, significantly reduces the reach of CSRF attacks.
Some users might find this multistep process cumbersome and tedious in systems requiring frequent changes, however, so use it when appropriate.
Design-based security features like these are ubiquitous on essential systems of administration and account management portals.
CSRF Token
Finally, the most potent mitigation policy we can implement is using CSRF tokens to validate every request coming from our clients.
These tokens work by linking the user session to server-generated tokens, which the server validates upon request.
The tokens are present in all forms as hidden fields. So, when the client proceeds to submit the form, it contains a validation voucher that the user intended this action.
To implement CSRF tokens in Node.js, we can use the "csurf" module for creating and validating tokens.
const cookieParser = require('cookie-parser'); // CSRF Cookie parsing
const bodyParser = require('body-parser'); // CSRF Body parsing
var csrf = require('csurf');
/**
* App Variables
*/
const app = express();
// Middlewares
var csrfProtect = csrf({ cookie: true })
app.get('/form', csrfProtect, function(req, res) {
// Generate a tocken and send it to the view
res.render('send', { csrfToken: req.csrfToken() })
})
app.post('/posts/create', parseForm, csrfProtect, function(req, res) {
res.send('data is being processed')
})
This change must be applied to the main app.js class file, making it look like the following:
Notice that we added the routing code here, but you can have it in a separate routing class "index.js" file.
Once you have done this, you can modify all your request and response routes to use tokens where needed.
Additionally, you must add the hidden field in all forms in your application.
Here's an example of a simple form:
<form action="/posts/create" method="POST">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
Title: <input type="text" name="title">
Text: <input type="text" name="text">
<button type="submit">Submit</button>
</form>
And that's about it.
Sustainable Security
Implementing and maintaining proper security measures are some of those ethereal values you can appreciate only when needed. But, unfortunately, most teams will have a hard time convincing their superiors of their importance.
Conversely, sophisticated attacks are becoming more widespread in this day and age. So, it's difficult not to justify the investment in reassurance and peace of mind provided by adequate security measures.
Thankfully, modern platforms like Node.js make it more available and effortless to keep security standards high. That way, you can focus on what matters: bringing value to your users.
This post was written by Juan Reyes. Juan is an engineer by profession and a dreamer by heart who crossed the seas to reach Japan following the promise of opportunity and challenge. While trying to find himself and build a meaningful life in the east, Juan borrows wisdom from his experiences as an entrepreneur, artist, hustler, father figure, husband, and friend to start writing about passion, meaning, self-development, leadership, relationships, and mental health. His many years of struggle and self-discovery have inspired him and drive to embark on a journey for wisdom.