🔥

CSRFについて

2024/10/06に公開

CSRFとは

クロスサイト・リクエストフォージェリを略してCSRFです

  • クロスサイト(Cross-Site):
    • 「サイトを跨ぐ」という意味
    • 攻撃が異なるウェブサイト間で行われることを示す
  • リクエスト(Request):
    • ウェブブラウザがウェブサーバーに送信する要求のこと
  • フォージェリ(Forgery):
    • 「偽造」や「詐称」を意味する
    • 正規のユーザーになりすまして不正なリクエストを作成することを指す

CSRF攻撃

ユーザーが意図しないリクエストをユーザーの認証情報を使って送信させる攻撃です。この攻撃により、攻撃者はユーザーが持つ特権(例: ログイン済みのセッションやクッキー)を利用して、不正な操作を実行させること。

脆弱性が生まれる原因

  • form要素のaction属性にはどのドメインのURLでも指定できる
    • クロスドメインアクセス(異なるオリジンへアクセスすること)はブラウザに同一オリジンポリシーがあるためできなくなっている。個人情報やセッションデータが不正に他のドメインに漏れないようにするため。これを意図的に許可するのがCORS(クロスオリジンリソースシェアリング)
      • CORSを設定しなくても、クロスドメインアクセスが許可されているものがいくつかある
        • その中に**「formのaction属性」**がある。クロスドメインアクセスができることでの脆弱性を利用して、CSRF攻撃ができる
  • クッキーに保管されたセッションIDは、対象サイトに自動的に送信される
    • 罠サイト経由のリクエストでもセッションIDのクッキー値が送信されるので、認証された状態で攻撃リクエストが送信される

パスワードが変更させられる例

  • ⓪利用者がexmple.jpにログインしている
  • ①攻撃者が罠を用意する
  • ②被害者が罠を閲覧する
  • ③罠のJavaScriptにより、被害者のブラウザ上で攻撃対象サイトに対し、新しいパスワードがPOSTメソッドにより送信される ※クッキーとして、攻撃対象サイトのセッションIDが不要されている
  • ④パスワードが変更される

対策

  • CSRF対策の必要なページを区別する
  • 正規利用者の意図したリクエストを確認できるように実装する

方法

  • 秘密情報(トークン)の埋め込み
  • パスワード再入力
    • 以下の要件を満たしているならパスワード再入力でCSRF対策は可能
      • 重要な処理に先立って、正規利用者であることを再確認する
      • 共有PCで別人が操作している状況などがなく、本当に正規の利用者であることを確認する
    • ただし、ログアウトなどの前に毎回パスワードを再入力することになるなどUXが悪くなる懸念もある
  • Refererのチェック
    • Refererと言うのはHTTPリクエストのリンク元のURLを示す情報
      • CSRF攻撃時のHTTPリクエストにおいてReferer以外は同じ情報を送信している状態になる
        • しかしRefererの値を通常のWebアプリケーションではチェックしないため、チェックするようにすることで対策をする手法
          • ただしこれをしてしまうと利用者がRefererが送信されないように設定している場合にそのページの実行ができなくなる懸念がある

方法の比較

トークンの埋め込み パスワード再入力 Referer確認
開発工数
利用者への影響 なし パスワード入力の手間 Refererをオフにしている利用者が使えない
推奨する利用シーン 一般的な対策方法であり、あらゆる場面で利用推奨 成りすまし対策や、確認を強く求めるような要件がある画面 利用者の環境を限定でkりう既存アプリケーションのCSRF対策

CSRFライブラリの使い方

こちらのライブラリを使用して実装します
前述した方法の「秘密情報(トークン)の埋め込み」の手法になります

https://www.npmjs.com/package/csrf

手順

インストール(TypeScriptを使用するため型定義もインストール)

npm install csrf @types/csrf

サーバー側でcsrfインスタンスを作成し、ミドルウェアとして設定する

app.ts

import Tokens from "csrf";

// csrfに関する箇所以外のコードは省略

// csrfインスタンスを作成
const tokens = new Tokens();

// CSRFミドルウェアを定義
app.use((req, res, next) => {
  const secret = tokens.secretSync();
  const token = tokens.create(secret);
  res.locals.csrfToken = token;
  res.cookie("csrf-secret", secret, { httpOnly: true });
  res.setHeader("xsrf-token", token);
  next();
});
  • secretとtokenの違い
    • secret(シークレット):
      • これは、サーバーサイドで生成される秘密の文字列
      • トークンを生成し、後で検証するために使用される
      • 外部に露出させてはいけない機密情報
    • token(トークン):
      • secretを使って生成される一意の文字列
      • クライアントに送信され、リクエスト時に送り返されるもの
      • 実際のリクエストに含まれる、検証可能な値
  • secretとtokenの生成プロセス
    • サーバーは各セッションごとにユニークなsecretを生成
    • このsecretを使用して、tokenを生成
    • secretはcookieに保存され、tokenはレスポンスヘッダーまたはレスポンスボディ(例:隠しフォームフィールド)で送信
  • リクエスト時の流れ
    • クライアントがリクエストを送信する際、cookieに保存されたsecretは自動的にサーバーに送信
      • 同時に、クライアントはtokenをリクエストヘッダーまたはリクエストボディに含めて送信
  • サーバーでの検証:
    • サーバーはリクエストからsecret(cookie)とtoken(ヘッダーまたはボディ)を取得
    • サーバーは受け取ったsecretを使用して、受け取ったtokenが有効かどうかを検証
      • シークレットはトークンを生成するための「種」として使用される
      • 同じシークレットから生成されたトークンのみが有効となる
  • ポイント
    • secretはtokenを生成・検証するための「鍵」のような役割を果たす

app.ts

app.post("/submit", (req, res) => {
  const secret = req.cookies["csrf-secret"];
  if (tokens.verify(secret, req.body._csrf)) {
    res.send("Success! Tokenが一致しました");
  } else {
    res.status(403).send("Errorになったよ! Tokenが一致しません");
  }
});
  • const secret = req.cookies["csrf-secret"];
    • リクエストのクッキーから 'csrf-secret' の値を取得している
    • この 'secret' は、前のレスポンスでサーバーがクライアントに送信したもの
    • httpOnly フラグ付きで保存されているため、JavaScriptからはアクセスできない
  • tokens.verify(secret, req.body._csrf)
    • tokens.verify() メソッドを使用して、CSRFトークンの検証を行っている
    • 第一引数 secret は、クッキーから取得したシークレット値
    • 第二引数 req.body._csrf は、フォームのhiddenフィールドから送信されたCSRFトークン
    • この関数は、シークレットを使用してトークンを検証し、有効な場合は true を返す
  • 条件分岐:
    • トークンが有効な場合(verify()true を返す)
      • "Success! Tokenが一致しました" というメッセージを送信する
    • トークンが無効な場合
      • HTTP状態コード 403 (Forbidden) と共に、"Errorになったよ! Tokenが一致しません" というメッセージを送信する

index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>CSRF Protection Demo</title>
</head>
<body>
    <h1>CSRF Protection Demo</h1>
    <form action="/submit" method="POST">
        <input type="hidden" name="_csrf" value="<%= csrfToken %>">
        <input type="text" name="data" placeholder="Enter some data">
        <button type="submit">Submit</button>
    </form>
</body>
</html>
  • hiddenフィールドにcsfrTokenを仕込む
  • cookieにsecretがあり、ブラウザのhiddenフィールドにはtokenを仕込んである状態になる
    • この2つが送信されて、サーバー側でtokenとsecretの整合性を検証することで、csrf対策になる
      • つまり罠サイト(secretから生成されたtokenを持たない)画面からのリクエストを弾くことができる

実装してみた画面

  • hiddenフィールドのinputにtokenが埋め込まれており、こちらの内容を削除したり編集するとsubmitできなくなる

成功パターン

失敗パターン(tokenを編集)

最後に

今、第2版 体系的に学ぶ 安全なWebアプリケーションの作り方 の書籍を読んでいて、今回csrfについてアウトプットしました。

最後まで読んでくださりありがとうございました!

バックテック【ヘルステック系スタートアップの試行錯誤】

Discussion