Open2

NestJS CSRF対策 やり方

happo31happo31

結論

function readCsrfToken(req) {
  return req.csrfToken();
}

async function bootstrap() {
  app.use(CookieParser());
  app.use(csurf({
    cookie: true,
    httpOnly: true,
    value: readCsrfToken
  }));
}
  • キモとしては↑のように csurf ミドルウェアの設定時にトークンを読み出す処理をカスタマイズ出来るよという話である。

事の顛末

NestJS で CSRF 対策したくなったのでやり方を調べた。
なお、この記事は通常の認証にはJWTを用い、そのJWTをクッキーに仕込むというやり方をしているようなプロダクトを想定。

公式には一応書いてはいるけど、これだけだとなぜか動かない。

https://docs.nestjs.com/security/csrf

以下引用

import * as csurf from 'csurf';
// ...
// somewhere in your initialization file
app.use(csurf());

動かないと書いたが、正確には csurf が csrf トークンの検証に失敗しているのが原因らしい。

何故かを追うために、 csurf のソースコードを見てみる。
すると、以下の部分で検証を行っているようだ。

csurf/index.js(110行目~)
// verify the incoming token
if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
  return next(createError(403, 'invalid csrf token', {
    code: 'EBADCSRFTOKEN'
  }))
}

ignoreMethod[req.method] は、コンフィグで設定が可能な「このリクエストメソッドでは検証をしない」というのを参照しているだけなので、重要そうな tokens.verify(secret, value(req)) を見ていく。

verify メソッドの定義は、 csrf/index.js にあるようだ。

csrf/index.js(124行目~)
Tokens.prototype.verify = function verify (secret, token) {

  // 中略

  var expected = this._tokenize(secret, salt)

  return compare(token, expected)
}

なるほど、要約すると渡ってきた token が期待される値と同じかを確認しているだけらしい。

ということは、(再度引用)

csurf/index.js(110行目~)
// verify the incoming token
if (!ignoreMethod[req.method] && !tokens.verify(secret, value(req))) {
  return next(createError(403, 'invalid csrf token', {
    code: 'EBADCSRFTOKEN'
  }))
}

この value(req) がキモということになる。
value はどこから来ているかというと、

csurf/index.js(51行目~)
  // get value getter
  var value = opts.value || defaultValue

なるほど、 options.value に渡してやったものが使われるようだ。
ちなみに、この defaultValue ではリクエストヘッダーを見て csrf トークンを探しているような実装になっているで、クライアント側でなんとかしてあげればデフォルト状態でも動くかもしれない。

というわけで、以下のように csrf トークンを req から読み出す関数を書いて app.use(csurf()) に渡してやるとよさそう。

function readCsrfToken(req) {
  return req.csrfToken();
}

async function bootstrap() {
  app.use(CookieParser());
  app.use(csurf({
    cookie: true,
    httpOnly: true,
    value: readCsrfToken
  }));
}

これを NestJS のbootstrap部分に入れてやると今度は動いているようだ。
(実行例とかを貼るとよいんだろうけど今すぐはちょっと用意できない)

req.csrfToken() の存在は、 req を console.log で出力して見つけたので、もしかしたらちゃんとしたドキュメントとか型定義がどこかにあるのかもしれない。

というドキュメントを漁るよりもソースコードを直接読んだら解決したパターンのお話でした。

happo31happo31

うーんよく考えたらJWTを使っててCookieで扱ってるならCSRF対策は別にしなくて十分なのか?