フューチャー技術ブログ

Go1.25 リリース連載 net/http - CSRF対策

はじめに

こんにちは。ペンギンになりたいエンジニアの島ノ江です。
現在は FutureVuls という脆弱性管理サービスの開発・営業などを担当しています。

Go 1.25リリース連載の4本目、マイナーアップデートの net/http での CSRF 対策の強化について触れます。

この記事では、以下の内容について触れていきます。CSRFについて既知の場合はリリースの実装内容の項目を参照してください。

  1. 今回のリリースの概要
  2. CSRFの概要
  3. issue提案の背景
  4. リリースの実装内容

関連する issue はこちらです(issue

今回のリリースの概要

新たに net/http に CrossOriginProtection が実装され、安全でない Cross-Origin ブラウザリクエストを拒否することで、CSRF から保護するようになりました。これまでの Go では、標準ライブラリの net/http に CSRF 対策用のハンドラは存在しなかったため、外部ライブラリの利用や開発者が一から実装する必要がありました。今回のリリースにより、これらが標準ライブラリとして含まれるようになりました。

具体的には、fetch meta data リクエストヘッダーである Sec-Fetch-Site ヘッダーの確認、または Origin ヘッダーのホスト名と Host ヘッダーを比較することで検出します。これにより、従来 CSRF 対策として利用されていたトークンや Cookie を必要とせずに、オリジンベース及びパターンベースで CSRF の対策ができるようになりました。

詳細な判定ロジックは後述します。

CSRFについて

Cross-Site Request Forgery(CSRF) とは、Web アプリケーションの脆弱性を利用した攻撃手法の1つです。Web サイト側が、ログインした利用者からのリクエストについて、その利用者が意図したリクエストであるかどうかを識別する仕組みを持たない場合に、ブラウザが Cookie を自動送信する性質が悪用されて、悪意のあるリクエストを受け付けてしまう場合があります。この脆弱性があると、ユーザが意図していない操作を Web サイト上で強制的に実行させることができます。

「Forgery」とは和訳すると「偽造」という意味で、文書や署名を偽造する行為を指します。

CSRFの攻撃条件

基本的に以下の3要素が揃うと攻撃条件が成立します。

  1. ユーザが認証済みの Web サイトが存在する
    ユーザは既にログインしており、セッションが有効な状態である
  2. Webサイトが状態を変更するリクエストを、ユーザからの認証情報のみで判断している
    サーバ側が、リクエストの送信元が本当にそのユーザ本人であるかの確認を行っていない
  3. 攻撃者が、ユーザに悪意のあるWebページやメールを閲覧させる
    攻撃者は、ユーザが認証済みのサイトに対して悪意のあるリクエストを仕込んだページを用意し、ユーザを誘導する

IPAに解説があるため、この図を引用します。

image

CSRFの対策

CSRF には古典的に以下のような方法で対策をとれます。

  1. CSRFトークンの利用
    すべての入力フォームに手動でトークンを埋め込み、都度検証する方法。ただし、各フォームで設定する必要があり、実装が面倒に問題がある
  2. Origin ヘッダーのチェック
    リクエストの送信元オリジンを確認する方法。ただし、リバースプロキシ環境などで設定が複雑になる問題がある
  3. SameSite 属性クッキーの利用
    Cookie に属性を設定し、クロスサイトリクエスト時にクッキーを送付するかどうかを制御する方法。ただし、一部のSSOフローで壊れる問題がある

CSRFの最近の動向

CSRF はWebサイト全体のセキュリティ向上に伴い、近年は被害件数が減少傾向にあります。

その背景は主に以下の通りです。

  1. Cookie の SameSite 属性の自動設定など、モダンブラウザでの自動対応
    Webサイト側で明示的な対策をしなくても、ブラウザ側で多くの CSRF が自動的にブロックされるようになりました
  2. Webフレームワーク側での対策の自動化
    開発者が意識して一から実装する必要はなくなりました。今回のリリース内容もこれに分類されます
  3. SPAとAPI通信を組み合わせた方式の普及
    SPA では Cookie による認証を使わずに、リクエストヘッダーに JWT のような認証トークンを入れる方式を採ります。そのため、そもそもの CSRF 攻撃の前提が成り立たなくなっています

上記のような対応が進んだことで、CSRF の脅威度は大きく下がりました。しかし、古いシステムや対策が不十分なWebサイトでは依然としてこのリスクは残るため、基本的なセキュリティ対策は引き続き必要となります。

提案の背景

これまで、CSRF 対策を目的とした Go のライブラリでは、例えば以下の2つが広く利用されていました。

gorilla/csrf では、上述の対策のうち1.CSRFトークンによる検証と2.Origin ヘッダーの検証で CSRF の対策をしていました。ただし、issue の提案者はこのライブラリに関して、Origin ヘッダーの検証に関するバグがある問題や、脆弱性の報告から修正までの対応が遅いなどの問題を指摘しています。

そこで、このようなライブラリに外部依存していた部分を標準ライブラリでサポートすることで、メンテナンス性やセキュリティアップデートをしやすくしたのが今回のアップデート対応です。

実装内容

CrossOriginProtection構造体

ベースとなる struct は以下のようになっています。

type CrossOriginProtection struct {
bypass atomic.Pointer[ServeMux]
trustedMu sync.RWMutex
trusted map[string]bool
deny atomic.Pointer[Handler]
}
設定項目 概要 設定メソッド
trusted(信頼済みオリジン) 安全とわかっている送信元オリジンを登録する AddTrustedOrigin
bypass(チェックの回避) CSRFチェックを完全に無効化するURLパスのパターン。公開APIなど、意図的に保護対象外にしたい場合に利用する AddInsecureBypassPattern
deny (拒否処理) リクエストブロック時の処理の設定 SetDenyHandler

具体的に使う際は、各メソッドで設定し、ハンドラを CrossOriginProtection で wrap します。
各フィールドを設定しない場合はデフォルトの設定で安全に動作します。

mux := http.NewServeMux()
csrfProtection := http.NewCrossOriginProtection()

// trusted: 信頼オリジンの設定(任意)
err := csrfProtection.AddTrustedOrigin("https://trusted-example.com")
if err != nil {...}

// bypass: チェックを回避する設定(任意)
csrfProtection.AddInsecureBypassPattern("/api/public/")

// deny: リクエストがブロックされた際のカスタム処理(任意)
denyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Origin Blocked: Origin=%s, Path=%s", r.Header.Get("Origin"), r.URL.Path)
http.Error(w, "Forbidden", http.StatusForbidden)
})
csrfProtection.SetDenyHandler(denyHandler)

// ハンドラを wrap する
srv := http.Server{
Addr: ":8080",
Handler: csrfProtection.Handler(mux),
}
log.Fatal(srv.ListenAndServe())

Cross-Originの判定方法

リクエストが Cross-Origin かどうかは Check メソッドで判定されます(実装)。判定ロジックを詳しくみていきます。

  • GET, HEAD, OPTIONS のリクエストは、サーバの状態を変更しない安全なメソッドのため、常に許可されます
  • 次に、リクエストヘッダーの Sec-Fetch-Site ヘッダーを優先的に確認します
    • "" の場合(Sec-Fetch-Site に対応していない古いブラウザからのアクセス、curl によるリクエストなど):後述の Origin の比較に移ります
    • same-origin, noneの場合:同一オリジンからのアクセスで安全と判断して許可します
    • 上記以外の cross-site などの場合は、例外ルールに当てはまるかを isRequestExempt メソッドで確認します。下記に当てはまる場合は、安全と判断します
      • リクエストのパスが、Cross-Originチェックを無視する「バイパスリスト(bypass)」に設定されている
      • リクエストの送信元が、安全だとわかっている送信元を記録する「信頼済みオリジンリスト(trusted)」に設定されている
  • Sec-Fetch-Site で判定ができない場合、リクエストヘッダーの Origin ヘッダーを確認します
    • "" の場合:同一オリジンリクエストか非ブラウザリクエストと判定して許可します
    • Origin ヘッダーと、Host ヘッダー(リクエスト先のホスト名)が異なる場合、Cross-Origin と判断します
  • 最後に、危険な可能性をはらむリクエストについて、isRequestExempt メソッドで例外的に許可できないかを確認します

なお、issue にも書かれていますが、この OriginHost の比較をする方法には問題があります。それは、 Host ヘッダーには scheme(http / https)が含まれないため、「http:// から https:// へのリクエストを区別できない」というものです。

例えば…

  • Origin ヘッダー: http://example.com
  • Host ヘッダー: example.com
  • リクエスト URL : https://example.com/

…のようなアクセスをする場合、Origin ヘッダーとHost ヘッダーは example.com で一致するため、このチェックを通過してしまいます。しかし、httphttps は異なるオリジンであり、本来はブロックするべきクロスオリジンリクエストです。

ただ、実装上この判定ロジックに回るのは、Sec-Fetch-Site に対応していない古いブラウザでアクセスした場合です。そのため、この次善策では fail-open として、リクエストをブロックして安全側に倒すのではなく、利便性のために甘く判定しています。

なお、httphttps のクロスオリジンについては、HSTS(HTTP Strict Transport Security)を利用して、ブラウザが常に https でアクセスするように設定することで解決できます。そもそも http:// のような暗号化されていないページでは、データを送信する前の段階で中間者攻撃にあう可能性があるため、セキュリティ上問題があります。

さいごに

今回はマイナーアップデートの内容をテーマに、Web セキュリティに関する CSRF 周りの理解を深めてみました。Cookie 認証の問題点や Sec-Fetch-Site などに関する理解を深められて、とても興味深かったです。

セキュリティの基本として、多層防御の観点は重要です。ブラウザの SameSite 属性を設定したうえで、さらにサーバー側で明示的にリクエストを検証することで、より堅牢なシステムを実現できます。セキュリティ対策の実現の学習としても調べていて面白かったです。

ご覧いただきありがとうございました。