visit
A CSRF attack is one that tricks a victim into submitting a malicious request — a request they did not intend to make — to a website where they are authenticated (logged in).
CSRF Attacks are blind — which means the attacker doesn't see what happens after the victim submits the request. CSRF attacks often target a state change on the server.
For example, say there is a bank that allows you to transfer money with the following endpoint. You just have to enter account
and amount
in the GET request to send money to a person as follows:
//bank.com/transfer?account=Mary&amount=100
# Sends 9999 to the Attacker's account
//bank.com/transfer?account=Attacker&amount=9999
<!-- Downloading this image triggers the GET request attack -->
<img
src="//bank.com/transfer?account=Attacker&amount=9999"
width="0"
height="0"
border="0"
/>
<!-- Fake link that triggers the GET request attack -->
<a href="//bank.com/transfer?account=Attacker&amount=9999"
>View my Pictures</a
>
Let's assume we have the same vulnerable endpoint and the attacker simply needs to enter the account
and amount
information to trigger the request.
POST //bank.com/transfer?account=Attacker&amount=9999
The attacker can create a form and hide the account
and amount
values from the user. People who click on this misrepresented form will send a POST request without them knowing.
<!-- Form disguised as a button! -->
<form action="//bank.com/transfer" method="POST">
<input type="hidden" name="acct" value="Attacker" />
<input type="hidden" name="amount" value="9999" />
<button>View my pictures</button>
</form>
<form>...</form>
<script>
const form = document.querySelector('form')
form.submit()
</script>
CSRF Attacks cannot be executed with PUT
or DELETE
requests because the technology we use doesn't allow them to.
CSRF Attacks cannot be executed via HTML forms because forms don't support PUT
and DELETE
requests. It only supports GET
and POST
. If you use any other method (except for GET
and POST
), browsers will automatically convert them into a GET request.
<!-- Form doesn't send a PUT request because HTML doesn't support PUT method. This will turn into a GET request instead. -->
<form action="//bank.com/transfer" method="PUT"></form>
Now here's a fun aside: How do people send PUT
and DELETE
request through a form if HTML doesn't allow it? After some research, I discovered most frameworks let you send a POST
request with a _method
parameter.
<!-- How most frameworks handle PUT requets -->
<form method="post" ...>
<input type="hidden" name="_method" value="put" />
</form>
You can execute a PUT
CSRF Attack via JavaScript, but the default prevention mechanism in browsers and servers today makes it really hard for these attacks to happen — you have to deliberately let down the defenses for it to happen. Here's why.
To execute a PUT
CSRF Attack, you need to send a Fetch request with the put
method. You also need to include the credentials
option.
const form = document.querySelector('form')
// Sends the request automatically
form.submit()
// Intercepts the form submission and use Fetch to send an AJAX request instead.
form.addEventListener('submit', event => {
event.preventDefault()
fetch(/*...*/, {
method: 'put'
credentiials: 'include' // Includes cookies in the request
})
.then(/*...*/)
.catch(/*...*/)
})
First, this request will NOT be executed by browsers automatically because of CORS. Unless — of course — the server creates a vulnerability by allowing requests from anyone with the following header:
Access-Control-Allow-Origin: *
Second, even if you allow all origins to access your server, you still need a Access-Control-Allow-Credentials
option for browsers to send cookies to the server.
Access-Control-Allow-Credentials: true
Third, even if you allow cookies to be sent to the server, browsers will only send cookies that have the sameSite
attribute set to none
. (These are also called third-party cookies).
This section is huge to take in. I've created a few more articles to help you understand exactly what's going on — and why it's so frigging impossibly hard to expose yourself to a PUT
CSRF Attack:
In short — you only have to worry about POST
CSRF Attacks unless you really screwed up your server.
What's important is that the CSRF Token must be a randomly generated, cryptographically strong string. If you use Node, you can generate the string with crypto
.
import crypto from 'crypto'
function csrfToken (req, res, next) {
return crypto.randomBytes(32).toString('base64')
}
If you use Express, you can place this CSRF token in your cookies like this. While doing so, I recommend using the sameSite
strict option as well. (We'll talk about sameSite
in a bit).
import cookieParser from 'cookie-parser'
// Use this to read cookies
app.use(cookieParser())
// Setting CSRF Token for all endpoints
app.use(*, (req, res) => {
const { CSRF_TOKEN } = req.cookies
// Sets the token if the user visits this page for the first time in this session
if (!CSRF_TOKEN) {
res.cookie('CSRF_TOKEN', csrfToken(), { sameSite: 'strict' })
}
})
<form>
with a CSRF Token — which would be included in the form's submission.
app.get('/some-url', (req, res) => {
const { CSRF_TOKEN } = req.cookies
// Render with Nunjucks.
// Replace Nunjucks with any other Template Engine you use
res.render('page.nunjucks', {
CSRF_TOKEN: CSRF_TOKEN
})
})
You can then use CSRF_TOKEN
form like this:
<form>
<input type="hidden" name="csrf" value="{{CSRF_TOKEN}}" />
<!-- ... -->
</form>
// Checks the validity of the CSRF Token
app.post('/login', (req, res) => {
const { CSRF_TOKEN } = req.cookies
const { csrf } = req.body
// Abort the request
// You can also throw an error if you wish to
if (CSRF_TOKEN !== csrf) return
// ...
})
credentials
to include
or same-origin
to include cookiesdocument.cookies
and add it as a request header.
// Gets the value of a named cookie
function getCookie () {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
if (match) return match[2]
}
// Sends the request
fetch('/login', (req, res) => {
credentials: 'include',
headers: {
'CSRF_TOKEN': getCookie('CSRF_TOKEN')
}
})
// Checks the validity of the CSRF Token
app.post('/login', (req, res) => {
const { CSRF_TOKEN } = req.cookies
const { CSRF_TOKEN: csrf } = req.headers
// Abort the request
// You can also throw an error if you wish to
if (CSRF_TOKEN !== csrf) return
// ...
})
Setting sameSite
to strict
the above example ensures that the CSRF Token cookie is only sent to the server if the request originates from the same website. This ensures that the CSRF Token will never be leaked to external pages.
You can — optionally but recommended — set the sameSite
attribute to strict
as you set the authentication cookie. This ensures that no CSRF Attacks can be conducted since the authentication cookies will no longer be included in cross-site requests.
Do you need CSRF Token protection if you used set sameSite
to strict
for your authentication cookie?
I would say no, in most cases — because sameSite
already protects the server from cross-site requests. We still need the CSRF token to protect against one particular type of CSRF: Login CSRF.
In the Login CSRF, the attacker tricks the user into logging in with the attacker's credentials. Once the attack succeeds, the user will continue to use the attacker's account if they're not paying attention.
<form action="//target/login" method="post">
<input name="user" value="Attacker" />
<input name="pass" type="password" value="AttackerPassword" />
<button>Submit</button>
</form>
const form = document.querySelector('form')
// Sends the request automatically
form.submit()
You can prevent both kinds of CSRF Attacks with the Double Submit Cookie pattern and the Cookie to header method. Setting sameSite
to strict
prevents normal CSRF but not Login CSRF.