CSRF Example
An overview of CSRF, SOP, & CORS
Cross-Site Request Forgery is an attack that allows an attacker to make authenticated calls to any websites the victim may be logged into. Here we run through an example scenario.
Follow code on Github: https://github.com/mohammedabdulwahhab/csrf_examples
Victim Webserver
Let's suppose the victim wants to log into his banking website. Our (simple) banking webserver has a GET endpoint for the home page and a POST endpoint for transfers. We use express to create this bank webserver.
const express = require('express')
const cookieParser = require('cookie-parser')
const bodyParser = require('body-parser')
const app = express()
const port = 3000
// We need cookies and body params
app.use(cookieParser())
app.use(bodyParser())
// Middleware function to authenticate into the webserver
function is_authenticated(req, res, next) {
console.log('Came here')
// Check cookies to determine if logged in
if (req.cookies['session'] == 'secret_token_placed_here'){
next()
}
else{
res.redirect('/auth')
}
}
app.get('/auth', (req, res) => {
console.log('Authenticating', req.query.username, req.query.password)
// User has valid login
if (req.query.username == 'username' && req.query.password == 'password'){
// Add cookie certifying user has authenticated
res.cookie('session', 'secret_token_placed_here', {
maxAge: 1000 * 60 * 15, // would expire after 15 minutes
httpOnly: false, // The cookie only accessible by the web server/not javascript
signed: false // Indicates if the cookie should be signed
})
res.redirect('/')
}
// Login was bad
else {
res.set('Content-Type', 'text/html');
res.status(200).send('Click <a href="/auth?username=username&password=password">here to login</a>')
}
})
app.get('/', is_authenticated, (req, res, next) => {
console.log('Logged into banking website')
res.set('Content-Type', 'text/html');
res.status(200).send("<h3>Welcome to your bank!</h3><form action='/transfer' method='post'><input id='POST-name' placeholder='recipient' name='recipient'><input id='POST-amount' placeholder='$20' name='amount'><input type='submit' value='Save'></form>")
})
app.post('/transfer', is_authenticated, (req, res) => {
console.log('Transferring money to', req.body.recipient, req.body.amount)
res.set('Content-Type', 'text/html');
res.status(200).send('<h3>Success! Go back to <a href="/">home</a></h3>')
})
app.listen(port, () => console.log(`Bank app listening on port ${port}!`))
Serve this webserver with node bank.js
and check that everything works:


Great! Our bank works as expected. Now let's see how a hacker could exploit our design to transfer money to himself.
Attacking Webserver
Let's take a look at how our bank checks that requests are originating from an authenticated user.
User Logs in: When the user logs in, the server asks that the browser stores a cookie named session. We see this happen in the handler for the /auth route
...
res.cookie('session', 'secret_token_placed_here', {
maxAge: 1000 * 60 * 15, // would expire after 15 minutes
httpOnly: false, // The cookie only accessible by the web server/not javascript
signed: false // Indicates if the cookie should be signed
})
...
2. User makes request:The GET '/' & POST '/transfer' routes are protected by the is_authenticated middleware. This middleware checks that the HTTP request came with a valid session cookie. If not, the server redirects them to GET /auth
...
// Middleware function to authenticate into the webserver
function is_authenticated(req, res, next) {
console.log('Came here')
// Check cookies to determine if logged in
if (req.cookies['session'] == 'secret_token_placed_here'){
next()
}
else{
res.redirect('/auth')
}
}
...
This authentication strategy works because the browser always sends the cookies set by an origin. In this case, any request to localhost:3000 will always have all the cookies set by that origin sent along by the browser. This property of cookies is exactly what a CSRF attack exploits to cause state changes.
In this case, any request to localhost:3000 will always have all the cookies set by that origin sent along by the browser. This property of cookies is exactly what a CSRF attack exploits to cause state changes.
All the attacker needs to do now is to trick a victim into clicking on a link to a different webpage. Once this happens, a form on that webpage could easily POST to the bank web-server. The browser will send along the session cookie stored in the browser. This type of request is indistinguishable from one intentionally initiated by the user from the perspective of the bank web server and thus succeeds.
Let's construct this attacking webserver
const express = require('express')
const app = express()
const port = 5000
app.get('/', (req, res) =>
{
console.log('I am a vicious website')
res.set('Content-Type', 'text/html');
// Malicious code
res.status(200).send("<h3>I am an innocent website!</h3><form action='http://localhost:3000/transfer' method='post'><input id='POST-name' placeholder='recipient' name='recipient' value='hacker'><input id='POST-amount' placeholder='$20' name='amount' value='10000'><input type='submit' value='Save'></form><script>document.forms[0].submit()</script>")
}
)
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Run the webserver with node attacker.js
Navigate to localhost:5000 to see our attack in action.
Protection against CSRF
The most common protection measure against this vulnerability is to enrich the form with a hidden token that is submitted along with the form. The server must do the added work of checking that this token is valid, but this will prevent against the CSRF attack. Let's see it in action. We modify the GET & POST routes in bank.js to include a csrf_token from the session
...
const hash = require('crypto-js').SHA256
function generate_csrf_token_from(session){
return hash(session).toString()
}
app.get('/', is_authenticated, (req, res, next) => {
console.log('Logged into banking website')
res.set('Content-Type', 'text/html');
// Send csrf token in hidden input
let csrf_token = generate_csrf_token_from(req.cookies['session'])
res.status(200).send("<h3>Welcome to your bank!</h3><form action='/transfer' method='post'><input id='POST-name' placeholder='recipient' name='recipient'><input id='POST-amount' placeholder='$20' name='amount'><input id='POST-csrf' value='" + csrf_token + "' name='csrf_token' type='hidden'><input type='submit' value='Save'></form>")
})
app.post('/transfer', is_authenticated, (req, res) => {
// if csrf token in form doesn'tvalidate - FUCK EVERYTHING UP
if (req.body.csrf_token != generate_csrf_token_from(req.cookies['session'])){
res.status(403).send('Bad request!')
return
}
console.log('Transferring money to', req.body.recipient, req.body.amount)
res.set('Content-Type', 'text/html');
res.status(200).send('<h3>Success! Go back to <a href="/">home</a></h3>')
})
...
If we navigate to localhost:5000 we see that our attack no longer works. Success!
What if the attacker used AJAX to get the hidden csrf_token?
This is a good question. What if the attacker simply included some AJAX in his webpage to retrieve the csrf associated with the session. Why is he not able to do this?
This brings us to the Same Origin Policy (SOP) of browsers. SOP prevents sites from making AJAX requests across origins. This means the webpage would not succeed in making the AJAX call mentioned previously. Let's add this to our attacker webpage script:
...
<script>
...
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState == XMLHttpRequest.DONE) {
alert(xhr.responseText);
}
}
xhr.open('GET', 'http://localhost:3000', true);
xhr.send(null);
</script>
...
We see that the request is denied. What is interesting to note is that the request still goes through (check this in the bank webserver) logs, but the response is blocked from the requestor.

CORS
Finally, we want to lax this policy in certain situations. For example, a REST API needs to be accessed across origins. We can permit this behavior by adding a Cross Origin Request Sharing header to our API. In express, the ''cors' package allows us to easily enable this.
var cors = require('cors')
var app = express()
app.use(cors())
Last updated
Was this helpful?