0

How can you do a CRSF attack on express if it only accepts JSON?

Sample Node app:

const express = require('express');

const app = express();

app.use(express.json());
 
app.post('/item', (req, res, next) => {
    console.log('posting item');
    res.send(`posted item name: ${req.body.itemName}`);
  });

app.listen(4000)
      .on('listening', console.log("HTTP server listening on port 4000"));

What I tried:

If you do an attack from an html form it sends form data:

<!DOCTYPE html>
<html lang="en">
  <body>
    <form action=http://localhost:4000/item method=post >  
        <input name ="itemName" value="Shirt" type="text">
     <input type=submit>  
       </form>  
 
    <script>
      document.forms[0].submit();
    </script>
  </body>
</html>

With this attack, req.body will be empty. express.json() only parses json, not form data. So in this instance, a POST where the end point requires user data, the attack doesn't do anything.

My actual app uses fetch to send requests. fetch does populate req.body but my understanding is that CSRF can't be done with fetch- Browsers send preflight requests when using fetch. If the preflight request shows that the origin isn't allowed, it will not send the full request. So it will always fail with a CSRF request. But I'm not so certain of this.

So is there another way to send a CSRF attack with user inputs? Or in this case, is there no way to do a successful csrf attack?

2

Browsers only send pre-flights when you make a "non-simple" request. A "simple" request is basically "anything an HTML form could send". Setting a content-type other than what a form can send (such as JSON) will trigger a preflight.

HOWEVER... in many cases, you can send arbitrary content as "text/plain" (which doesn't trigger a pre-flight), and the server will parse it anyhow. There are also ways to send valid JSON from an HTML form as x-www-form-urlencoded (with weirdly-named fields and values, so you need something that isn't strict about allowed elements) sometimes, but sending text is almost certainly better. Most JSON parsers don't actually check what the reported content type is, they just try to parse the content as JSON. If it's parseable, it passes. Browsers can do that just fine, using fetch or XHR.

2

Express express.json() only populates the body as JSON if the Content-Type: application/json is used:

$ curl http://127.0.0.1:4000/item -XPOST -d 'itemName=42' -H"Content-Type: application/x-www-form-urlencoded"
posted item name: undefined
$ curl http://127.0.0.1:4000/item -XPOST -d '{"itemName":42}' -H"Content-Type: application/json"
posted item name: 42

The browser cannot set Content-Type: application/json in cross-origin requests without CORS. As a consequence this endpoint should be kind-of not vulnerable to CSRF.

Note however that the endpoint function is still called. You may still be vulnerable to CSRF attacks if your code accepts to process the request without any JSON.

Examples:

app.post('/dumpAllData',  (req, res, next) => {
    res.send(backupService.backup({
        since: res.body.since, // optional
    }));
});
app.post('/deleteEverything', (req, res, next) => {
    deletionService.deleteEverything({
        recursive: req.body.recursive ? true : false
    });
    res.send("OK");
});

My actual app uses fetch to send requests. fetch does populate req.body but > my understanding is that CSRF can't be done with fetch-

You can definitely to CSRF with fetch. However, unless the target endpoints enabled more stuff using CORS, you can only use the following Content-Type:

  • application/x-www-form-urlencoded
  • multipart/form-data
  • text/plain
3
  • Oh nice, it's even secure against silliness like considering text/plain; charset=utf-8,application/json to be a valid JSON type, which fools a lot of home-grown attempts at this.
    – CBHacking
    Jun 18 at 8:27
  • @CBHacking, text/plain; charset=utf-8,application/json looks like a nice trick :)
    – ysdx
    Jun 18 at 9:26
  • Yeah, I've used that a few times; lots of stuff just looks for a simple substring match. I actually went and looked up the source code for express (and then through two other repos, because NPM is Like That) on github to verify how it was implementing the content-type filter. It explicitly filters out parameters of the types. This is not surprising for such a central library but I wouldn't have been very surprised if it was vulnerable after all since it relies on code from so many people subject to who-knows-how-much review; see above comment about the NPM ecosystem.
    – CBHacking
    Jun 18 at 9:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.