【问题标题】:CSRF doesn't work on the first post attemptCSRF 在第一次发布尝试时不起作用
【发布时间】:2021-02-28 08:07:03
【问题描述】:

这是我第一次实现 CSRF,也是第一次在 Stack 上发帖。我一直在努力通过 CSRF 配置,但终于得到了几乎可以工作的东西。

如果我在新浏览器中打开带有书签的页面并提交表单,我会看到 403 Invalid CSRF 错误:EBADCSRFTOKEN。在这种情况下,用户身份验证是 cookie,因此它不会挑战。我想知道会话是否已过期?后续帖子工作正常。获取请求都很好。我很困惑,是时候把这个挑战放在一边并寻求帮助,因为我已经做了太久了,不胜感激。

Server.js 不引用 csrf 中间件但建立会话

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();
});

索引.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) {
  ...
});

接口,我用的是EJS,相关代码:

<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>

以上代码按预期呈现:

<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">

令牌通过 post 数据中的表单 post 发送为 _csrf: qBdXd2ZQ-8vnbyw_IUivar_6UKp0qvhUf290

【问题讨论】:

  • 无关提示:您正确使用了const,所以也要使用let(而不是var)。

标签: javascript node.js express csrf


【解决方案1】:

编辑 1

TL;DR

你在 cookie 模式下使用了两次 csrf 中间件,它第一次使 express set cookie 两次。 (你可以看到有两个令牌)
你给你的 ejs token1,但你的快递在这行 app.use(csrf({ cookie: true })) 中用 token2 验证它。

这就是发生无效令牌的原因。
但是,它只是第一次发生,因为根本原因取决于 csurf 包的实现。
如果您想了解根本原因,可以查看解释以获得更多详细信息。

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')
})

那么如何解决呢?

删除var csrfProtection = csrf({ cookie: true });这一行。
并删除路由器中的“设置”中间件

来自

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

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

说明

很抱歉粘贴了这么长的代码,但我需要它来解释为什么你的代码在第一次发布时就被破坏了。

请看下面代码中的mark1和mark2。

Mark1:你使用app.use(csrf({ cookie: true })),这意味着无论路径匹配与否,你都为路由器的其余部分提供了一个中间件。

Mark2:您只在您的特定路由器中提供csrfProtection,即“/settings”路径,而不是路由器的其余部分。

如果一起使用呢?
Express 将在第一时间为您两次设置cookie。

// 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>

这就是你第一次得到无效 csrf 令牌的原因。

然后看csurf的源码。
secretnullundefined 时,它将使用setSecret 方法,这是第一次发生。
你可以在这里看到 csurf 使用setHeader

// 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)
}

您可以在 csurf 模块的 index.js 的第 105 行添加 console.log("secret: " + secret)
然后您将看到以下日志,其中 set-cookie 被触发了两次

secret: undefined
secret: undefined

这就是你第一次发帖失败的原因,因为你同时使用了以下两种方法。

// ====> 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')
})

原答案

我不确定你在寻找什么。

但是,如果您在谈论为什么 GET 请求不受 csurf 保护,因为 GET 请求默认被 csurf 忽略。 你可以在here看到源代码

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

顺便提一下,GET request 通常用于只读操作。

OWASP CSRF

不要使用 GET 请求进行状态更改操作。

我认为这是 csurf 默认不保护 GET 请求的原因。

【讨论】:

  • 我理解为什么 GET 请求有效,但感谢您的解释。我试图弄清楚为什么我在新会话中的第一篇文章失败了。 ..但现在我想知道我是否有一个会话但它已过期并且在下一次刷新或发布之前不会获得新的会话。所以基本上,也许失败的帖子正在发生,因为我有一个过期的会话。
  • 你能再重现一下这种情况吗?还是只是偶尔发生?
  • 您可以通过在 csurf 包中的 index.js 中插入一些 console.log 进行调试(index.js 中的第 110-112 行),以便在第一次发布时获得无效的 csrf 令牌时查看 csrf 值.
  • 你的代码没问题。但是如果你不设置 cookie 的 maxAge,它就永远不会过期。所以不能是过期的问题。
  • 我能够重现,我发现如果我将 csrf({ cookie: true }) 更改为 false 就可以解决问题。所以我认为这与使用会话和 cookie 来验证 cookie 的冲突有关。在文档中建议使用其中一个:expressjs.com/en/resources/middleware/csurf.html
猜你喜欢
  • 2016-10-17
  • 2019-04-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-06-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多