はじめに
GOの修正対応プロジェクトにて、セキュリティ向上のためcsrfトークンにsamesite属性を付与するよう依頼されたのですが、躓いたため共有します。
EchoV4.2.0 以降ではCSRFConfigにCookieSameSite設定がすでに追加されているため、あくまでもそれ以前のバージョンでの一時的な解決策となります。
そもそもsamesite属性とは?(知っている方は飛ばしてください)
SameSite属性とは、Webブラウザがクッキーを送信する範囲を制御するための設定です。主にクロスサイトリクエストフォージェリ(CSRF)攻撃を防ぐために利用されます。
Strict: 同一サイトからのリクエストでのみクッキーを送信する(最も厳格)。
Lax: 基本的には同一サイトのみだが、トップレベルナビゲーション(リンク遷移など)などの安全なリクエストでは他サイトからでも送信される(現在のブラウザのデフォルト挙動)。
None: 制限なし(Secure 属性が必須)。
本題CSRFWithConfigではsamesite属性がつけられない
ご存知の通り、Echoを使用してミドルウェアにてCSRFトークンを定義する際には middleware.CSRFWithConfig() にて下記のように定義することが可能です。
このConfigの中に CookieSameSite: http.SameSiteLaxMode のように記載できれば良いのですが、v4.1.13以前のEchoでは定義自体が存在せず、コンパイルエラーとなるため不可能です。
// CSRF設定
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLength: 32,
TokenLookup: "form:x-csrf-token",
ContextKey: "csrf",
CookieName: "csrf",
CookieMaxAge: 0,
CookieSecure: true,
CookieHTTPOnly: true,
//ここにsamesiteの記載は出来ない
}))
ダメだった解決策
最初単純に、クッキー設定したなら、そのままクッキー取得 → samesite属性上書きしたら終わりやんと思っていました。
しかし、結論から言うと c.Cookie("csrf") はあくまでリクエストのクッキー(ブラウザから送られてきたもの)を取得する処理です。 CSRFWithConfig で設定されるCSRFクッキーはレスポンスヘッダー(これからブラウザに送るもの)に設定されているため、この方法では取得・変更ができませんでした。
// CSRF設定
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLength: 32,
TokenLookup: "form:x-csrf-token",
ContextKey: "csrf",
CookieName: "csrf",
CookieMaxAge: 0,
CookieSecure: true,
CookieHTTPOnly: true,
//ここにsamesiteの記載は出来ない
}))
csrf, _ := c.Cookie("csrf")
//❌ここでメモリ上からcsrfトークンのクッキーを取得して上書きすればええやん?
なので下記のようにレスポンスヘッダーからSet-Cookieを取得し直接書き換えをする必要があります。
// CSRF設定
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLength: 32,
TokenLookup: "form:x-csrf-token",
ContextKey: "csrf",
CookieName: "csrf",
CookieMaxAge: 0,
CookieSecure: true,
CookieHTTPOnly: true,
// ※Echoのバージョンが古い等の理由で CookieSameSite が設定できない場合
}))
// 【修正版】CSRF CookieにSameSite=Laxを強制的に追加するミドルウェア
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// レスポンス書き込み前にフックする
c.Response().Before(func() {
cookies := c.Response().Header()["Set-Cookie"]
if len(cookies) == 0 {
return
}
// 書き換えが発生したかどうかのフラグ
modified := false
newCookies := make([]string, 0, len(cookies))
for _, cookie := range cookies {
// クッキー名が "csrf" で始まり、かつ SameSite 属性がない場合のみ追記
// ※CookieName設定を変えた場合はここも変更が必要
if strings.HasPrefix(cookie, "csrf=") && !strings.Contains(strings.ToLower(cookie), "samesite") {
cookie += "; SameSite=Lax"
modified = true
}
newCookies = append(newCookies, cookie)
}
// 書き換えがあった場合のみヘッダーを更新
if modified {
c.Response().Header().Del("Set-Cookie")
for _, cookie := range newCookies {
c.Response().Header().Add("Set-Cookie", cookie)
}
}
})
return next(c)
}
})
c.Response().Before(...): ステータスコードやヘッダーが書き込まれる直前に処理をフックします。
strings.HasPrefix(cookie, "csrf="): 全てのクッキーを書き換えないよう、対象のCSRFクッキーのみを判定します。
文字列操作: 既存のクッキー文字列のお尻に "; SameSite=Lax" を追記します。
これで、古いバージョンのEchoでも強制的にSameSite属性を追加することができました!