1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

なんだか名前だけ聞いたことあるCSRFにつて調べたことの概念とGoでの実装例を記事にしたものです。

CSRFとは?

CSRF(Cross-Site Request Forgery)は、悪意のある第三者のWebサイトから、ユーザーが意図しないリクエストを送信させる攻撃手法です。
例として以下のような攻撃シナリオが挙げられます。

  1. ユーザーが正規のWebサイトにログインしている
  2. ログインによるセッションを保持した状態で、同じブラウザを使用して悪意のあるWebサイトにアクセスする
  3. 悪意のあるサイトに埋め込まれたスクリプトやフォームにより、ユーザーの意図しないリクエストなどが実行される

この攻撃が成功する理由は、ブラウザが自動的にCookieを送信する仕様を悪用しているためです。ユーザーが認証済みの状態であれば、攻撃者は正規のセッションを利用してリクエストを送信できてしまいます。

CSRFトークンとは?

CSRFトークンは、この攻撃を防ぐための対策の一つです。このトークンの手段としては以下のような例が挙げられます。
まず、サーバーサイドで予測不可能な文字列を生成します。それをセッションとしてクライアントに送り、フォームのhidden項目やHTTPヘッダーとして次回のPOSTなどのリクエストと一緒に送信させます。最後に、送信されたリクエストをサーバーサイドで検証し、そのトークンが正しければ内部動作を走らせるようにします。

CSRFトークンによる保護の仕組みは以下の通りです:

  1. サーバーは正規のフォームを生成する際に、ランダムなトークンを生成し埋め込む
  2. クライアントからリクエストを受け取った際に、トークンの存在と値を検証する
  3. トークンが不正な場合はリクエストを拒否する

この仕組みにより、攻撃者は正規のトークンを知ることができないため、不正なリクエストを送信できなくなります。

Goで実装してみる

実際にGoでCSRFトークンのコードを作成すると以下のようなものになります。

1. 必要なパッケージのインポート

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gorilla/sessions"
	"github.com/joho/godotenv"
	"log"
	"net/http"
)

一部パッケージの説明

  • crypto/rand: ランダムなデータを生成するために使用します。CSRFトークン生成に利用
  • encoding/base64: バイナリデータを Base64 エンコードするために使用します。トークンを安全に文字列として扱うために利用
  • github.com/gorilla/sessions: セッション管理のためのパッケージ。セッションストアを使ってユーザーの状態を管理します

2. CSRFトークン生成関数

 func generateToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

generateToken()はランダムな文字列を作成する関数です。この生成した文字列をトークンとして使用します。

3. セッション作成とCSRFトークンの設定

// セッション生成とCSRFトークンの設定
func createSession(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "csrf-session")
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	token := generateToken()
	session.Values["csrf_token"] = token

	if err := session.Save(r, w); err != nil {
		fmt.Fprintf(w, "error: Failed to save session: %s", err)
		return
	}

	// トークンを返す(ここでは単に通知として返す)
	fmt.Fprintf(w, "Session created. CSRF Token: %s", token)
}

createSession は、セッションを作成し、CSRFトークンを生成してセッションに保存し、トークンをレスポンスとして返す関数です。このレスポンスとして返された関数はクライアント側でヘッダーに保存されることを想定しています。
流れとしては以下のようになります。

  • store.Get(r, "csrf-session") でセッションを取得
  • generateToken() で新しいトークンを生成
  • セッションにトークンを格納し、session.Save(r, w) でセッションデータを保存
  • 最後に、生成された CSRF トークンをレスポンスとして返す

4. CSRFミドルウェア

// CSRFトークンを検証するミドルウェア
func csrfMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, err := store.Get(r, "csrf-session")
		if err != nil {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
			// ヘッダーからCSRFトークンを取得
			formToken := r.Header.Get("X-CSRF-Token")

			// セッションに保存されているトークンを取得
			sessionToken, ok := session.Values["csrf_token"].(string)

			if !ok || formToken == "" || formToken != sessionToken {
				http.Error(w, "Invalid CSRF Token", http.StatusForbidden)
				return
			}
		}

		next(w, r)
	}
}

csrfMiddleware は、CSRF攻撃を防ぐためのミドルウェアです。
このミドルウェアは以下のようなものを実装しています。

  • store.Get(r, "csrf-session") で、リクエストに関連するセッションを取得
  • r.Header.Get("X-CSRF-Token") で、リクエストヘッダーから送られた CSRF トークンを取得
  • セッション内のトークンとヘッダーのトークンを比較し、異なる場合はリクエストを拒否します(HTTP 403 Forbidden を返します)
  • トークンが正しければ、リクエスト処理を次に渡す

5. JSONリクエストを処理するハンドラ

// JSONリクエストを処理するハンドラ
func submitForm(w http.ResponseWriter, r *http.Request) {
	var requestData map[string]string
	if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	message := requestData["message"]
	fmt.Fprintf(w, "Received message: %s", message)
}

submitFormは、POST リクエストで送られた JSON データを処理します。
json.NewDecoder(r.Body).Decode(&requestData) でリクエストボディを JSON としてデコードし、message フィールドを取り出して表示します。

6. main関数の説明

func main() {
	// .env ファイルをロード
	if err := godotenv.Load(); err != nil {
		log.Fatal("Error loading .env file")
	}

	store = sessions.NewCookieStore([]byte(os.Getenv("SECRET_KEY"))) // セッションストアの作成
	store.Options = &sessions.Options{
		HttpOnly: true,
		Secure:   true, // HTTPS通信のみに送信される
		MaxAge:   600,  // クッキーの有効期限(10分)
		SameSite: http.SameSiteStrictMode,
	}

	http.HandleFunc("/create-session", createSession)
	http.HandleFunc("/submit", csrfMiddleware(submitForm))

	log.Println("Starting server on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

main関数は以下のようなものを実装しています。

  • .env ファイルをロードし、環境変数を読み込むために godotenv.Load() を呼び出し
  • sessions.NewCookieStore([]byte(os.Getenv("SECRET_KEY"))) でセッションストアを初期化
  • トークンを発行するためのhttp.HandleFunc("/create-session", createSession) と メッセージをPOSTするhttp.HandleFunc("/submit", csrfMiddleware(submitForm)) で、リクエストパスとハンドラを関連付け
  • http.ListenAndServe(":8080", nil) でサーバーをポート8080で起動

実装コード完成系

main.go
package main

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gorilla/sessions"
	"github.com/joho/godotenv"
	"log"
	"net/http"
	"os"
)

var store *sessions.CookieStore

// CSRFトークンを生成する関数
func generateToken() string {
	b := make([]byte, 32)
	rand.Read(b)
	return base64.StdEncoding.EncodeToString(b)
}

// CSRFトークンを検証するミドルウェア
func csrfMiddleware(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, err := store.Get(r, "csrf-session")
		if err != nil {
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			return
		}

		if r.Method == "POST" || r.Method == "PUT" || r.Method == "DELETE" {
			// ヘッダーからCSRFトークンを取得
			formToken := r.Header.Get("X-CSRF-Token")

			// セッションに保存されているトークンを取得
			sessionToken, ok := session.Values["csrf_token"].(string)

			if !ok || formToken == "" || formToken != sessionToken {
				http.Error(w, "Invalid CSRF Token", http.StatusForbidden)
				return
			}
		}

		next(w, r)
	}
}

// セッション生成とCSRFトークンの設定
func createSession(w http.ResponseWriter, r *http.Request) {
	session, err := store.Get(r, "csrf-session")
	if err != nil {
		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		return
	}

	token := generateToken()
	session.Values["csrf_token"] = token
	store.Options = &sessions.Options{
		HttpOnly: true, // JavaScriptからアクセスできないようにする
		Secure:   true, // HTTPS通信のみに送信されるようにする
		MaxAge:   3600, // クッキーの有効期限(例:1時間)
	}
	if err := session.Save(r, w); err != nil {
		fmt.Fprintf(w, "error: Failed to save session: %s", err)
		return
	}

	// トークンを返す(ここでは単に通知として返す)
	fmt.Fprintf(w, "Session created. CSRF Token: %s", token)
}

// JSONリクエストを処理するハンドラ
func submitForm(w http.ResponseWriter, r *http.Request) {
	var requestData map[string]string
	if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}

	message := requestData["message"]
	fmt.Fprintf(w, "Received message: %s", message)
}

func main() {
	// .env ファイルをロード
	if err := godotenv.Load(); err != nil {
		log.Fatal("Error loading .env file")
	}

	store = sessions.NewCookieStore([]byte(os.Getenv("SECRET_KEY"))) // セッションストアの作成
	store.Options = &sessions.Options{
		HttpOnly: true,
		Secure:   true, // HTTPS通信のみに送信される
		MaxAge:   600,  // クッキーの有効期限(10分)
		SameSite: http.SameSiteStrictMode,
	}

	http.HandleFunc("/create-session", createSession)
	http.HandleFunc("/submit", csrfMiddleware(submitForm))

	log.Println("Starting server on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
.env
SECRET_KEY=secret-key

secret-keyは、以下のコマンドで簡単に作成できます。

 openssl rand -base64 32

このコードは基本的な実装例であり、実際のアプリケーションでは、以下の点に注意する必要があります:

  • トークンの有効期限設定
  • 適切なエラーハンドリング
  • HTTPSの使用

動作テスト

エンドポイント

# POST
http://localhost:8080/submit

# GET
http://localhost:8080/create-session

一旦はトークンがまだ生成されていない状態でPOSTをしてみた結果が以下のものになります。CSRF トークンがないのでエラーが出ていますね。
スクリーンショット 2024-12-22 0.04.25.png

実際にトークンを取得します。

スクリーンショット 2024-12-22 0.07.19.png
レスポンスで生成されたトークンが返ってきました。
返ってきたトークンをHeaderKeyX-CSRF-TokenValueをトークンとして追加します。

スクリーンショット 2024-12-22 0.13.07.png

この状態でPOSTのリクエストを送ってみてください。

{
    "message": "アドベントカレンダー"
}

スクリーンショット 2024-12-22 0.17.37.png

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?