如何在node.js/express中测试受csrf保护的端点

13

我已经在express中实现了csrf(跨站点请求伪造)保护,像这样:

...
app.use(express.csrf());
app.use(function (req, res, next) {
  res.cookie('XSRF-TOKEN', req.csrfToken());
  next();
});
...

这很好用。AngularJS在通过$http服务进行的所有请求中都使用了csrf令牌。我通过我的Angular应用程序发送的请求非常好用。

我的问题是测试这些API端点。我正在使用mocha运行自动化测试,并使用request模块测试我的api端点。当我使用request模块发出一个利用csrf(POST,PUT,DELETE等)的端点请求时,它会失败,尽管它正确地使用了cookie等。

有没有其他人找到了解决方案? 是否有任何需要更多信息的人?

测试示例:

function testLogin(done) {
  request({
    method: 'POST',
    url: baseUrl + '/api/login',
    json: {
      email: 'myemail@email.com',
      password: 'mypassword'
    } 
  }, function (err, res, body) {
    // do stuff to validate returned data
    // the server spits back a 'FORBIDDEN' string,
    // which obviously will not pass my validation
    // criteria
    done();
  });
}

你能提供一些你尝试过的测试样例吗? - jjperezaguinaga
3个回答

11

诀窍在于您需要将POST测试包装在GET中,并从cookie中解析必要的CSRF令牌。 首先,这假定您创建了一个类似于以下内容的可用于Angular的CSRF cookie:

.use(express.csrf())
.use(function (req, res, next) {
  res.cookie('XSRF-TOKEN', req.session._csrf);
  res.locals.csrftoken = req.session._csrf;
  next();
})

那么,你的测试可能会像这样:

describe('Authenticated Jade tests', function () {
  this.timeout(5000);

  before(function (done) {
    [Set up an authenticated user here]
  });

  var validPaths = ['/help', '/products'];

  async.each(validPaths, function (path, callback) {
    it('should confirm that ' + path + ' serves HTML and is only available when logged in', function (done) {
      request.get('https://127.0.0.1:' + process.env.PORT + path, function (err, res, body) {
        expect(res.statusCode).to.be(302);
        expect(res.headers.location).to.be('/login');
        expect(body).to.be('Moved Temporarily. Redirecting to /login');

        var csrftoken = unescape(/XSRF-TOKEN=(.*?);/.exec(res.headers['set-cookie'])[1]);
        var authAttributes = { _csrf: csrftoken, email: userAttributes.email, password: 'password' };

        request.post('https://127.0.0.1:' + process.env.PORT + '/login', { body: authAttributes, json: true }, function (err, res) {
          expect(res.statusCode).to.be(303);

          request.get('https://127.0.0.1:' + process.env.PORT + path, function (err, res, body) {
            expect(res.statusCode).to.be(200);
            expect(body.toString().substr(-14)).to.be('</body></html>');

            request.get('https://127.0.0.1:' + process.env.PORT + '/bye', function () {
              done();
            });
          });
        });
      });
    });

    callback();
  });
});

这个想法是实际登录并使用从cookie中获取的CSRF令牌进行发布。请注意,您需要在mocha测试文件的顶部拥有以下内容:

var request = require('request').defaults({jar: true, followRedirect: false});

这看起来像是能够实现它的那个。我希望我不必像那样嵌套请求,所以在运行测试时可能会禁用csrf。 - tytho
诚然,这有点麻烦,但如果您忽略CSRF,您就无法真正进行端到端测试。 - Dan Kohn
没错...我也考虑过使用不同的测试框架,比如phantomjs,这样我就可以测试应用程序更多的交互而不仅仅是端点,但我想做两者都是好的。感谢你的帮助! - tytho
它与node.js高度兼容!感谢您分享有关cookie使用的想法! - Ehsan
如果XSRF-TOKEN cookie不可用,且您不想在服务器上添加它,则可以使用res.text.match(/name=_csrf value=([^>]*)>/)[1]从登录页面的HTML表单中获取csrf令牌。 - Raul Santelices

1

@dankohn的答案非常有帮助。自那时以来,关于supertest和csurf模块都有了一些变化。因此,除了那个答案之外,我发现还需要将以下内容传递给POST:

  it('should ...', function(done) {
    request(app)
      .get('/...')
      .expect(200)
      .end(function(err, res) {
        var csrfToken = unescape(/XSRF-TOKEN=(.*?);/.exec(res.headers['set-cookie'])[1]);
        assert(csrfToken);
        request(app)
          .post('/...')
          .set({cookie: res.headers['set-cookie']})
          .send({
            _csrf: csrfToken,
            ...
          })
          .expect(200)
          .end(done);
      });
  });

1

我的做法是只在非生产环境中公开CSRF令牌:

if (process.env.NODE_ENV !== 'production') {
  app.use('/csrf', function (req, res, next) {
    res.json({
      csrf: req.csrfToken()
    })
  })
}

然后将其作为第一个测试,并将其保存为全局变量。您需要在测试中使用代理,以便始终使用相同的会话。


那么我该如何在上面的请求中使用csrf令牌呢?如果我向请求对象添加jar: true它将使用cookies,我可以看到token和cookie被使用了,但出于某种原因它不接受它。那么我该如何使用token在请求中使其接受呢? - tytho
你必须提交 x-csrf-token: csrf 或将其添加到请求正文中。是的,这很烦人,但这正是用户必须做的。确保会话在每个请求中不会更改。 - Jonathan Ong
我已经在测试端和服务器端记录了请求和响应对象,而且令牌在连续的请求中是相同的。当我使用前端网站访问相同的端点时,它就像原来一样存在。Cookie也存在,并在第一个请求之后的每个请求中添加到请求体中。 - tytho
这种“烦恼”是好的,因为这是一个测试,可以确保表单确实需要csrf来验证,而不是忽略它。我不喜欢这种方法的原因是你必须事先发出请求。但我想不出一个好的方法来获取(或存根)令牌,而不启动会话。 - calamari

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接