10月15日、Alex Edwards氏が「A modern approach to preventing CSRF in Go」と題したブログ記事を公開し、注目を集めている。
この記事では、Go 1.25で標準ライブラリに追加されたhttp.CrossOriginProtection
ミドルウェアを核に、トークンベース検査なしでCSRF(より厳密にはCORF: Cross-Origin Request Forgery)を防ぐ“現代的アプローチ”について詳しく紹介している。以下に、その内容を紹介する。
要旨
Go 1.25のhttp.CrossOriginProtection
は、ブラウザが付与するSec-Fetch-Site
およびOrigin
ヘッダーを用いて、同一オリジン以外からの“非セーフメソッド”(POST/PUT等)を自動で403として拒否する仕組みである。条件を満たせば、従来のダブルサブミットクッキーやgorilla/csrf
等の外部パッケージに依存せず、標準機能だけで堅牢な防御が可能になるという見立てである。
http.CrossOriginProtection
の基本
同ミドルウェアは以下の順序で送信元を判定する。
- まず
Sec-Fetch-Site
を確認し、same-origin
以外なら拒否する。 Sec-Fetch-Site
が無い場合はOrigin
とHost
を比較し不一致なら拒否する。- どちらのヘッダーも無い場合は「ブラウザ起源でない」とみなし、通過させる。
- 検査対象は非セーフメソッドのみで、GET/OPTIONS等のセーフメソッドは常に通過である。
最小実装は以下のとおりだ。
// File: main.go
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
slog.Info("starting server on :4000")
// http.NewCrossOriginProtection で mux をラップして起動
err := http.ListenAndServe(":4000", http.NewCrossOriginProtection(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
}
挙動のカスタマイズも可能で、信頼オリジンの追加や拒否時ハンドラーの差し替えに対応する。
// File: main.go
package main
import (
"fmt"
"log/slog"
"net/http"
"os"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", home)
slog.Info("starting server on :4000")
err := http.ListenAndServe(":4000", preventCSRF(mux))
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
}
func preventCSRF(next http.Handler) http.Handler {
cop := http.NewCrossOriginProtection()
cop.AddTrustedOrigin("https://foo.example.com")
cop.SetDenyHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("CSRF check failed"))
}))
return cop.Handler(next)
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello!")
}
制約と注意点
http.CrossOriginProtection
は近年のブラウザを前提とした防御である。Sec-Fetch-Site
またはOrigin
を送らない古いブラウザ(概ね2020年以前)からの攻撃には無力である。また、Origin
とHost
の比較にフォールバックする場合、Host
にはスキームが含まれないため、http://{host}
→https://{host}
のクロスオリジンを許してしまうケースがある。これを避けるにはHSTSの有効化が推奨される。加えて、本機構が最大効力を発揮するには本番でHTTPS(信頼できるオリジン)を用いる必要がある。
TLS 1.3の強制による前提の固め方
記事ではTLS 1.3を最低バージョンとして強制する前提を採用すると、接続可能な主要ブラウザはいずれもSec-Fetch-Site
またはOrigin
に対応している、という互換性データを確認している。結果として以下が成り立つ。
- TLS 1.3非対応の旧ブラウザはそもそも接続できない。
- 対応ブラウザは上記ヘッダーを送るため、
http.CrossOriginProtection
が有効に働く。
例外としてFirefox 60–69(2018–2019)はSec-Fetch-Site
非対応かつPOSTでOrigin
を送らず、同ミドルウェアが効かない。利用率はほぼ0%とされるが、リスクはゼロではない。主要ブラウザ以外(Chromium/Firefox派生以外)についても不確実性が残るため、完全性を保証するものではない。
Cross-siteとCross-originの差
- 同一オリジンは「スキーム・ホスト名・ポート」が完全一致。
- 同一サイトは「スキームと登録可能ドメイン(eTLD+1)」が一致。
例)https://example.com
、https://www.example.com
、https://login.admin.example.com
は同一サイトだが、オリジンは異なる。
http.CrossOriginProtection
はクロスオリジンを遮断する。ゆえに、同一サイト内の他サブドメイン(例:blog.example.com
→admin.example.com
)からの不正リクエストもブロックできる点が有用である。
SameSiteクッキーの位置づけ
SameSite=Lax
またはStrict
を付与したクッキーはクロスサイト送信されない。主要ブラウザでTLS 1.3を強制する前提ではSameSiteの互換性も揃っており、Firefox 60–69に対するクロスサイト由来のリスクを補完できる。ただし、SameSiteは“クロスオリジン(同一サイト内サブドメインからの攻撃)”を完全には防がない点に注意が必要である。
実運用の設計方針(まとめ)
記事は、トークン不要のCSRF/CORF対策が妥当化されうる前提条件を次のように整理している。
- 本番はHTTPSで、TLS 1.3を最低バージョンとして強制する。古いブラウザの接続不可を受容する。
- セーフメソッド(GET/HEAD/OPTIONS/TRACE)で状態変更を行わない。
- **
http.CrossOriginProtection
+SameSite=Lax/Strict
**を併用する(Firefox 60–69や一般的な多層防御のため)。 - 同一サイト内の他サブドメインからのCORFリスクはゼロでないため、サブドメイン群の有無や堅牢性を確認する。
- オリジンのHTTP版を提供しない(リダイレクトも含めて)。提供するならHSTSを必ず有効化する。
- TLS 1.3対応だが
Origin
/Sec-Fetch-Site
/SameSite
を満たさない非メジャーブラウザのリスクは不確実であり、サービス価値や被害規模に応じて受容可否を判断する。
以上を満たす場合、トークンベース検査の運用コストや実装複雑性を避けつつ、標準機能中心で現実的な防御線を構築できる。ただし、対象ユーザー層・対応ブラウザ範囲・サブドメイン構成によっては、従来型トークンと併用する選択が依然として合理的である。
詳細はA modern approach to preventing CSRF in Goを参照していただきたい。