CSRF doesn't work on the first post attempt

老子叫甜甜 提交于 2021-01-27 06:27:28

问题


This is my first time implementing CSRF and first time posting on Stack. I've struggled through the CSRF config, but finally got something that almost works.

If I open a bookmarked page in a fresh browser and submit a form, I'm seeing a 403 Invalid CSRF error: EBADCSRFTOKEN. In this case, the user auth is cookied so it does not challenge. I wonder if the session is expired? Subsequent posts work fine. Get requests are all fine. I'm stumped, time to put this challenge aside and ask for help as I've been at it for way too long, would appreciate any help.

Server.js does not reference csrf middleware but sets up the session

const express = require("express");
const path = require("path");
const favicon = require("serve-favicon");
const cookieParser = require("cookie-parser");
const bodyParser = require("body-parser");
const flash = require("connect-flash");
const mongoose = require("mongoose");
const logger = require("morgan");
const expressSession = require("express-session");
const setCurrentUser = require("./app/controllers/setCurrentUser");

var session = {
  secret: "XXXHIDDENXXX",
  cookie: {},
  resave: false,
  saveUninitialized: false,
};

app.use(favicon(path.join(__dirname, "public/images", "favicon.png")));
app.use(logger("dev"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(flash());
app.use(expressSession(session));
app.use("/public", express.static("public"));
app.use(setCurrentUser);

app.use((req, res, next) => {
  res.locals.user = req.user;
  res.locals.success = req.flash("success");
  res.locals.error = req.flash("error");
  next();
});

Index.js

"use strict";

var express = require("express");
var router = express.Router();
const csrf = require("csurf");
const isLoggedIn = require("../controllers/auth");

var csrfProtection = csrf({ cookie: true });

router.use(csrf({ cookie: true }));

router.use((req, res, next) => {
  // generate one CSRF token to every render page
  res.locals.token = req.csrfToken();
  next();
});

router.get("/settings", isLoggedIn, csrfProtection, function (req, res, next) {
  ...
});

router.post("/settings", isLoggedIn, csrfProtection, function (req, res, next) {
  ...
});

Interface, i'm using EJS, relevant code:

<head>
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<%= locals.token %>">
</head>

<form method="POST" action="/settings">
<input type="hidden" name="_csrf" value="<%= locals.token %>">
<div class="form-group">
<label for="displayName">Name</label>
<input type="text" name="displayName" id="displayName" maxlength='30' data-parsley-maxlength='30' class="form-control" value="<%= user.displayName %>" required>
</div>
<button class="btn btn-primary">Submit</button>
</form>

The above code renders as expected:

<meta name="csrf-param" content="authenticity_token">
<meta name="csrf-token" content="qBdXd2ZQ-8vnbyw_IUivar_6UKp0qvhUf290">
<input type="hidden" name="_csrf" value="qBdXd2ZQ-8vnbyw_IUivar_6UKp0qvhUf290">

Token is sent via the form post in the post data as _csrf: qBdXd2ZQ-8vnbyw_IUivar_6UKp0qvhUf290


回答1:


EDIT 1

TL;DR

You use csrf middleware twice in cookie mode, it make express set cookie twice at the first time. (you could see there are two tokens)
You give your ejs token1, but your express validate it with token2 in this line app.use(csrf({ cookie: true })).

That's why invalid token happened.
But, it only happen at the first time because root cause depend on the implementation of csurf package.
You could see explanation to get more detail if you want to know the root cause.

app.use(csrf({ cookie: true }))
app.use((req, res, next) => {
  // token1
  res.locals.token = req.csrfToken();
  next();
});
const csrfProtection = csrf({ cookie: true })
//                   token2 
app.get('/settings', csrfProtection, function (req, res) {
  res.render('send')
})

So how to fix it?

Remove var csrfProtection = csrf({ cookie: true }); this line.
And remove the middleware in your router which is "settings"

From

router.get("/settings", isLoggedIn, csrfProtection, function (req, res, next) {
  ...
});

To

router.get("/settings", isLoggedIn, function (req, res, next) {
  ...
});

Explaination

Sorry for pasting this long code, but I need it to explain why your code is broken in first time post.

Please see mark1 and mark2 in following code.

Mark1: you use app.use(csrf({ cookie: true })) that means you provide a middleware for rest of router no matter path matches or not.

Mark2: you only provide the csrfProtection in your specific router which is "/settings" path, not rest of router.

What if you use it together?
Express will set-cookie twice for you in the first time.

// server.js
const cookieParser = require('cookie-parser')
const csrf = require('csurf')
const bodyParser = require('body-parser')
const express = require('express')
const session = require("express-session")
const app = express()
const sess = {
  secret: 'Key',
  resave: false,
  saveUninitialized: true,
  cookie: {}
}
app.set("view engine", "ejs")
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use(session(sess))

// ====> mark1
app.use(csrf({ cookie: true }))
app.use((req, res, next) => {
  res.locals.token = req.csrfToken();
  next();
});

// ====> mark2
const csrfProtection = csrf({ cookie: true })

// ====> mark2
app.get('/settings', csrfProtection, function (req, res) {
  res.render('send')
})

// ====> mark2
app.post('/settings', csrfProtection,  function (req, res) {
  res.send('data is being processed')
})
app.listen(8080)
<!-- my send.ejs -->
<form method="POST" action="/settings">
  <input type="hidden" name="_csrf" value="<%= locals.token %>">
  <button class="btn btn-primary">Submit</button>
</form>

That's why you got the invalid csrf token in first time.

Then look at source code of the csurf.
It will use setSecret method when secret is null or undefined which is happening at first time.
And you could see csurf use setHeader here.

// here, it's a middleware you used.
return function csrf (req, res, next) {
  // .... other code

  // generate & set secret
  if (!secret) {
    secret = tokens.secretSync()
    setSecret(req, res, sessionKey, secret, cookie)
  }
}

// setSecret will go to this method
function setCookie (res, name, val, options) {
  var data = Cookie.serialize(name, val, options)

  var prev = res.getHeader('set-cookie') || []
  var header = Array.isArray(prev) ? prev.concat(data)
    : [prev, data]

  res.setHeader('set-cookie', header)
}

You could add console.log("secret: " + secret) in line 105 in index.js of csurf modules
then you'll see the following log which set-cookie is triggered twice.

secret: undefined
secret: undefined

That's why your first time post is broken because you use both following methods at the same time.

// ====> method1
app.use(csrf({ cookie: true }))
app.use((req, res, next) => {
  res.locals.token = req.csrfToken();
  next();
});

// ====> method2
const csrfProtection = csrf({ cookie: true })
app.get('/settings', csrfProtection, function (req, res) {
  res.render('send')
})

Original Answer

I'm not sure what you're looking for.

But if you're talking about why GET request is not protected by csurf , because GET request is ignored by csurf in default. You could see the source code at here

// ignored methods
var ignoreMethods = opts.ignoreMethods === undefined
    ? ['GET', 'HEAD', 'OPTIONS']
    : opts.ignoreMethods

By the way, GET request is usually used in read-only operations.

OWASP CSRF says

Do not use GET requests for state changing operations.

I think that is a reason why csurf doesn't protect the GET request in default.



来源:https://stackoverflow.com/questions/64869650/csrf-doesnt-work-on-the-first-post-attempt

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!