CSRF, or cross-site request forgery, is one of the most notoriously difficult exploits to mitigate in the world of development. Not only are these attacks everywhere on the web, but their potential for damage is quite astounding. This is why it's so important for people to be aware of their presence and to know how to protect their systems.
The purpose of this article is to serve as a starting point for developers in general and Node.js engineers in particular for CSRF protection. We will briefly explain what cross-site request forgery is, list some examples of CSRF attacks that you might find in the wild, and give you some mitigation strategies against them in Node.js.
Bear in mind that although we are focusing on Node.js development stacks in this article and the examples are written in JavaScript, the basic strategies apply for any technology stack. You are not required to have mastery of Node.js. However, a basic understanding of JavaScript is required. If you need a refresher on these, we encourage you to read the Node.js guides and familiarize yourself with the information.
Let's jump right in.
Explaining CSRF
Cross-site request forgery, or CSRF/XSRF, is an attack that relies on the user's privileges by hijacking their session. This strategy allows an attacker to circumvent our security by essentially deceiving the user into submitting a malicious request on behalf of the attacker.
CSRF attacks are possible because of two things. First, CSRF attacks exploit the user's inability to discern whether a seemingly legitimate HTML element contains malicious code. Second, since these attacks come from legitimate users, the protection mechanisms do not apply. This allows bad actors to dupe users into harming themselves.
What's more, bad actors are capable of masking HTML elements and can use social engineering tactics through avenues like chat or emails to significant effect. Moreover, these exploits seem to come from trusted sources for the average user, hijacking their trust in the systems they rely on day to day.
CSRF Attacks
To illustrate more accurately how a cross-site request forgery attack can hijack a system, let's explore the following example.
A user receives an email from what seems like a trusted source. Say an attacker has emulated the look of a banking institution and managed to mask the email to look legitimate. The victim, our non-tech-savvy auntie, sees the email, which conveys an urgent need for her to click on a provided link to check on an unusual transaction that the bank has flagged as suspicious.
Auntie then proceeds to hastily oblige and click on the link without verifying the authenticity of its source and 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, there are myriad things that could have happened. For example, when our victim clicked on the malicious link, a targeted URL could have taken advantage of a fresh 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.
Of course, there is a lot more in the rabbit hole of cross-site request forgery. Exploring the intricacies of how it exploits vulnerabilities in the web goes outside 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.
Mitigating CSRF Vulnerabilities in Node.js
So now that we have seen how easy it is to seize our user's sessions by hijacking their trust with sly social engineering tactics to wreak havoc in our systems, let's explore some mitigating measures against them.
Routing Structure
First, we must reconsider the design of the routing structure of our site. Simple CSRF attacks take advantage of systems that accept GET requests that perform a state change. This pattern is already an unsafe practice, and you should avoid it in all scenarios.
Luckily, fixing this is quite simple 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:
Lines of code in an integrated development environment.
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. Additionally, 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
Second, we advise all developers to implement confirmation mechanisms into all critical state-changing actions. Simply implementing a middle step between a submit, requiring the user to confirm their action, can significantly reduce the reach of CSRF attacks. Note that some users might find this multistep process cumbersome and tedious in systems requiring frequent changes. Design-based security features like these are ubiquitous on essential systems of administration and account management portals.
CSRF Token
Lastly, we must use 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 confirms 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')
})
You must apply this change to the main app.js class file, making it look something like the following:
Lines of code in an integrated development environment.
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 proceed to modify all your request and response routes to use tokens.
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>
Bottom Line
Cross-site request forgery is one of the most widespread exploits on the web.
Web platforms are exposed to them constantly, and many victims fall prey to their traps. Unfortunately, due to the nature of the attack, no platform is wholly protected from CSRF since a valid end user's authentication can be hijacked to reach the system. This makes it a game of risk management.
As in any area of technology, there are several ways to solve a problem. The process to mitigate the risks of exploitation from the platform perspective involves targeting the most prominent vulnerabilities and gradually increasing the friction for attackers to target our systems.
It's essential to make sure that we as developers take all steps necessary to mitigate the risks that we can handle and protect our users from bad actors.
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.