🎄

Go1.25からCSRF対策のメソッドを見てみる

に公開

はじめに

Webアプリケーションを開発する際に、CSRF(Cross-Site Request Forgery)攻撃への対策は非常に重要です。Go言語の最新バージョンであるGo1.25では、CSRF対策のための新しいメソッドが導入されました。本記事では、その新機能について詳しく解説します。

なぜ、気になるのか?

A. 推しパッケージだから

そして、また、一歩、net/httpだけで、Web APIをセキュアに実装できるようになったから。

背景

proposed Issue

https://github.com/golang/go/issues/73626

導入された背景をAIに要約してもらいました

  • 既存ライブラリの課題
  • 従来の対策手法が複雑

など、がありそうで、サードパティーのGinやEchoでも対策はできるけど、標準パッケージでも対応したほうがいいよねということになっていそうです。

違っていたら申し訳ございません。

AIによる要約
# CSRF対策のためのGoの新しいハンドラ提案

## 背景
CSRF(クロスサイトリクエストフォージェリ)は、攻撃者がユーザーのブラウザに不正なリクエストを送信させ、ユーザーのCookieを悪用する攻撃です。Cookie認証を使用するほぼすべてのアプリケーションがCSRF対策を必要とします。

## 主な問題点
- **Same-site vs Same-origin**: 同じサイト内でも異なるオリジン間には信頼レベルの差があり、特にHTTPとHTTPSの違いは重大
- **既存ライブラリの課題**: 
  - `gorilla/csrf`: HTTPS検出の不具合があり、メンテナンスが不十分
  - `justinas/nosurf`: セキュリティチェックが機能せず、メンテナンス停止状態

## 従来の対策手法
1. **CSRFトークン**: クラシックな方法だが、全てのフォームへの実装が必要
2. **Originヘッダー**: ブラウザが送信元を示すが、プロキシ環境で問題が発生
3. **SameSite Cookie**: 設計上クロスオリジン保護ではなく、広範な互換性問題で展開が失敗
4. **Fetch Metadata**: 2023年以降、全主要ブラウザで利用可能な推奨手法

## 提案内容
`net/http`に新しい`CrossOriginForgeryHandler`を追加:

- **主な保護**: `Sec-Fetch-Site`ヘッダーを使用してクロスオリジンの非安全なブラウザリクエストを拒否
- **フォールバック**: `Sec-Fetch-Site`がない場合、`Origin`ヘッダーと`Host`ヘッダーを比較
- **安全なメソッド**: GET/HEAD/OPTIONSは常に許可
- **柔軟性**: SSO等のために`UnsafeAllowCrossOrigin`関数や`BypassOrigins`で例外設定が可能

この提案により、開発者は追加実装なしで効果的なCSRF保護を実現できます。

CSRF攻撃とは?

CSRF攻撃は、悪意のあるウェブサイトがユーザーのブラウザを利用して、ユーザーが認証されている別のウェブサイトに対して不正なリクエストを送信させる攻撃手法です。これにより、ユーザーの意図しない操作が行われる可能性があります。

CSRF攻撃のイメージ
nano bananaによるイメージ画像

今までの対策方法

これまでのCSRF対策としては、以下のような方法が一般的に用いられてきました。

フレームワークなしの場合

  • CSRFトークンの利用
方法

サーバー側で一意で予測不可能なトークンを生成し、セッションに保存します。このトークンを、保護したいフォームの隠しフィールドやHTTPヘッダーに含めてクライアントに送信し、リクエスト時にトークンを検証する方法です。

  • Cookieの SameSite 属性の利用
方法

Cookieに SameSite 属性(主に Lax または Strict)を設定し、クロスサイトリクエスト時にはCookieがブラウザから送信されないように制御する方法です。

  • Origin ヘッダーや Referer ヘッダーのチェック
方法

リクエストの送信元を示す OriginReferer ヘッダーをチェックし、自サイトのオリジンからのリクエストでのみ処理を受け付けるようにする方法です。

Ginの場合

Ginには標準でCSRF対策ミドルウェアが付属していませんが、サードパーティのパッケージを利用するのが一般的です。

Goの標準のnet/httpパッケージをラップしたミドルウェアを利用するか、フレームワークに依存しない純粋なGoのCSRFライブラリ(例:github.com/gorilla/csrfなど)をGinのミドルウェアとして統合することが一般的です。

CookieのSameSite属性を、Ginのセッション管理ミドルウェアを通じて適切に設定することが基本的な防御策となります。

Echoの場合

Echoは、コアパッケージ内でいくつかの便利なミドルウェアを提供しており、その中にCSRF対策も含まれています。

labstack/echo/middleware.CSRF()

https://github.com/labstack/echo/blob/master/middleware/csrf.go

Echoの標準ミドルウェアパッケージに含まれています。

CSRFトークンを用いた防御機構を提供します。

ミドルウェアがセッショントークンを生成し、Cookieに保存します。

同時にリクエストトークンを生成し、HTTPヘッダーまたはフォームフィールドに含めるように要求します。

リクエストが来た際、この二つのトークンが一致するか検証することで攻撃を防ぎます。

設定例: どのヘッダー名やフォームフィールド名を使用するか、Cookieの属性(SameSiteなど)をどうするかを細かく設定できます。

Go1.25でできるようになること

Go 1.25の CrossOriginProtection は、モダンブラウザの Fetch metadataを利用し、上記のようなトークンやCookieを必要とせずにCSRF対策を提供することで、開発者の負担を大幅に軽減します。これは、従来の対策で課題となっていた「手動実装の複雑さ」「設定の漏れ」「Cookieやヘッダー依存による制約」を解消するものです。

追加された構造体・関数

ドキュメント

https://pkg.go.dev/net/http#CrossOriginProtection

追加された構造体

  • CrossOriginProtection

exportされたフィールドは持っていません。

追加された関数

  • func NewCrossOriginProtection() *CrossOriginProtection

    • 初期化関数。まず、これで、CrossOriginProtection構造体を初期化します。
  • func (c *CrossOriginProtection) AddInsecureBypassPattern(pattern string)

    • 指定されたパターンに一致するリクエストを許可する
    • パターンの一致の優先度はServeMuxと同じ
    • パスのリダイレクトなど追加される末尾の/の有無などは区別される
  • func (c *CrossOriginProtection) AddTrustedOrigin(origin string) error

    • AddTrustedOriginは、指定された値と完全に一致するOriginヘッダーを持つすべてのリクエストを許可する
    • Originヘッダーの値は "scheme://host[:port]" の形式
    • AddTrustedOriginは、他のメソッドやリクエスト処理と並行して呼び出すことができ、将来のリクエストに適用されます。
  • func (c *CrossOriginProtection) Check(req *Request) error

    • リクエストに対してクロスオリジンチェックを適用します。リクエストが拒否されるべき場合は、エラーを返す
  • func (c *CrossOriginProtection) Handler(h Handler) Handler

    • ハンドラhを呼び出す前に、クロスオリジンチェックを行うハンドラを返す
    • リクエストがクロスオリジンチェックに失敗した場合、リクエストは403 Forbiddenステータスで拒否されるか、CrossOriginProtection.SetDenyHandlerに渡されたハンドラによって処理される
  • func (c *CrossOriginProtection) SetDenyHandler(h Handler)

    • リクエストが拒否された際に呼び出されるハンドラを設定します。デフォルトのエラーハンドラは、403 Forbiddenステータスを返します。
    • 他のメソッドやリクエスト処理と並行して呼び出すことができ、以降のリクエストに適用されます。
    • Checkメソッドはエラーハンドラを呼び出しません。

実装例

標準パッケージのみで、最小で実装するなら以下で良いはず

main.go
func main() {
    engine := http.NewServeMux()

    engine.HandleFunc("/health", Csrf(healthCheck))

	srv := &http.Server{
		Addr:    "8080",
		Handler: engine,
	}

    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal(err)
		}
}
middleware.go
func Csrf(next http.HandlerFunc) http.HandlerFunc {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		cop := http.NewCrossOriginProtection()
		if err := cop.Check(r); err != nil {
			http.Error(w, err.Error(), http.StatusForbidden)
			return
		}
		next.ServeHTTP(w, r)
	})
}

ここに、AddInsecureBypassPatternAddTrustedOriginで許可設定などをオプションで追加できる形になるかと思います。

また、Handler(h Handler) Handlerを使用するならこんな感じかと思います。

複数のエンドポイントに適用

main.go
func main() {
    cop := http.NewCrossOriginProtection()
    cop.AddTrustedOrigin("https://example.com")
    
    // 複数のハンドラに適用
    http.Handle("/api/users", cop.Handler(http.HandlerFunc(handleUsers)))
    http.Handle("/api/posts", cop.Handler(http.HandlerFunc(handlePosts)))
    
    http.ListenAndServe(":8080", nil)
}

カスタム拒否ハンドラを設定

main.go
func main() {
   cop := http.NewCrossOriginProtection()

	// カスタムエラーハンドラを設定
	cop.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusForbidden)
		json.NewEncoder(w).Encode(map[string]string{
			"error": "CSRF protection failed",
		})
	}))

	handleProtected := func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
	}

	handler := cop.Handler(http.HandlerFunc(handleProtected))
	http.Handle("/api/protected", handler)
	http.ListenAndServe(":8080", nil)
}

まとめ

今回は、Go1.25で推しパッケージであるnet/httpに追加されたCrossOriginProtectionについて記事を書いてみました。

まぁ、でも、これがうれしいのは、Webフレームワークを作る開発者もしくは、Goの標準パッケージのみでWeb APIを作ってみたいとかいうクレイジーな人であって一般のWeb開発者が嬉しい類のものではないなと感じました。

しかし、こうやってよりセキュアに簡単に一般の開発者やライブラリの開発者がWeb APIやWebフレームワークを開発できるように言語仕様として追加されたのはとても良いことだなとも感じました。

Discussion