CSRF保護ミドルウェアの欠落

クロスサイトリクエストフォージェリ(CSRF)

説明

CSRFはログイン済みのセッションを悪用し、ユーザーが意図しない状態変更リクエスト(POST、PUT、PATCH、DELETEなど)を送信させる脆弱性です。ExpressでCookieやセッションによる認証を使いながら適切なCSRF対策を行わないと、攻撃者が悪意あるページへユーザーを誘導し、フォームやAJAXリクエストを送信させる可能性があります。その結果、ユーザーのセッション権限でアカウント変更、決済、設定変更などが行われるおそれがあります。

想定される影響

  • メールアドレスやパスワードの変更、送金などの不正な操作
  • アカウント情報や設定の意図しない変更、それに伴う機密情報の露出
  • 不正な決済、注文、権限変更による金銭的・業務上の損失

対処方法

  • Cookieやセッションによる認証には、保守されているCSRFミドルウェアかフレームワークの組み込み保護を、全体(app.use)または対象ルートに適用してください。導入前に保守状況を確認してください。
  • 状態を変更するPOST、PUT、PATCH、DELETEのルートを保護してください。
  • サーバーが生成したCSRFトークンをフォームのhiddenフィールドや X-CSRF-Token などのAJAXヘッダーで送り、サーバーで検証してください。
  • GETリクエストで状態を変更しないでください。
  • SameSite=strict/lax のCookie、重要なエンドポイントでのReferer/Origin検証、必要に応じたCAPTCHAや再認証も追加してください。

例

変更前

javascript
const express = require("express");
const session = require("express-session");

const app = express();
// アプリケーションを直接公開せず、信頼できる単一のTLSプロキシの背後で実行します。
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
  throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
  session({
    secret: sessionSecret,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, sameSite: "lax" },
  })
);

// 変更前: セッション認証にCSRF保護ミドルウェアがありません。
app.post("/account/email", (req, res) => {
  const userId = req.session.userId;
  const newEmail = req.body.email;
  // ... データベースのメールアドレスを更新します。
  res.send("updated");
});

app.listen(3000);

変更後

javascript
const express = require("express");
const session = require("express-session");
const { csrfSync } = require("csrf-sync");

const { csrfSynchronisedProtection, generateToken } = csrfSync({
  getTokenFromRequest(req) {
    if (req.is("application/x-www-form-urlencoded")) {
      return req.body.csrfToken;
    }
    return req.headers["x-csrf-token"];
  },
});

const app = express();
// アプリケーションを直接公開せず、信頼できる単一のTLSプロキシの背後で実行します。
app.set("trust proxy", 1);
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
const sessionSecret = process.env.SESSION_SECRET;
if (!sessionSecret || sessionSecret.length < 32) {
  throw new Error("SESSION_SECRET must contain at least 32 characters");
}
app.use(
  session({
    secret: sessionSecret,
    resave: false,
    saveUninitialized: false,
    cookie: { httpOnly: true, secure: true, sameSite: "lax" },
  })
);

// すべての状態変更ルートにCSRFトークン検証を適用します。
app.use(csrfSynchronisedProtection);

// フォームはhiddenの`csrfToken`フィールド、JSONリクエストは`x-csrf-token`ヘッダーで送ります。
app.get("/account/email", (req, res) => {
  res.json({ csrfToken: generateToken(req) });
});

// 変更後: トークンのないリクエストを拒否します。
app.post("/account/email", (req, res) => {
  const userId = req.session.userId;
  const newEmail = req.body.email;
  // ... データベースを更新します。
  res.send("updated");
});

app.listen(3000);

説明:

  • 変更前: 信頼できるTLSプロキシ、環境変数から取得した十分に長いセッション用の秘密値、安全なCookie属性だけでは、CSRF対策の代わりにはなりません。トークン検証がないと、同一サイトからの攻撃などで状態変更リクエストを偽造される可能性が残ります。
  • 変更後: 同じプロキシとセッションの設定に、採用したCSRFミドルウェアを全体に適用し、サーバー発行のトークンを検証します。この例は信頼できる単一のTLSプロキシの背後で動かす構成です。有効なCSRFトークンのない状態変更リクエストは拒否されます。

参考資料