CookieのSameSite属性とSecure属性について

初めまして。株式会社PRVENT開発部、バックエンドチームの横川です。
現在入社2年目で、普段はGoやRubyを書いて仕事してます。
今回はCookieSameSite属性Secure属性について紹介したいと思います。

なぜ書こうと思ったか

現在、『Mymonitor』という弊社のスタッフと取引先が利用するプロダクトのリプレースを進めており、Cookie初心者の自分がCookieを使ったログイン管理を実装しました。
そこで直面したエラーのおかげでCookieについて調べ上げCookieを完全に理解したので、今回直面したエラーの原因でもあるSameSite属性とSecure属性の挙動について記事にすることにしました。

そもそも Cookieとは

ブラウザで何かのサイトを閲覧した際にそのサイトのサーバーが発行してブラウザへ送信, 保存するテキスト情報です。
そのテキスト情報は、次またそのサイトへアクセスした際に毎回自動でブラウザからサーバーへ送信されるようになっていて使い方は様々です。
具体的にはサーバー側からのレスポンスにSet-Cookieヘッダを付与することでブラウザにセットされます。 スクリーンショット 2022-04-28 17.44.06.png (54.2 kB)

ログアウトできない...

MymonitorはフロントエンドにNext.js, バックエンドにGoを採用しており分業体制で開発を進めています。
ある程度機能を作ったところで一旦AWSに載せてみることになったのですが、ローカル環境では問題なく動作していたログアウトがAWS環境だと動作しませんでした...
ただこのエラーを機に、Cookieについて調べ上げることとなります。

原因はSamesite属性とSecure属性

確認したところ、サーバー側で発行するログアウト時のCookieにSameSite属性とSecure属性を設定していなかったのが原因でした。
ちなみにログイン、ログアウト時の意図した挙動は以下です。

機能 詳細
ログイン 有効期限が3時間のCookieをサーバー側で発行しブラウザに保存する
ログアウト 有効期限が切れたCookieをサーバー側で発行しブラウザに保存する
認証 リクエストの度にサーバー側でCookieを受け取りその値でアクセスコントロール

有効期限が切れたCookieは、ブラウザに保存すると同時に消えることでログアウトを実現できます。
今回は消えるはずのCookieがログアウト後も残ってしまっていました。

その時のソースコード

// ログイン時のcookie
cookie := http.Cookie{Name: name, Value: token, Expires: time.Now().Add(3 * time.Hour), HttpOnly: true, Secure: true, SameSite: http.SameSiteNoneMode}

// ログアウト時のcookie
cookie := http.Cookie{Name: name, Value: "", HttpOnly: true, MaxAge: -1}

すっぽり抜けてます。
ただ、この時Cookie初心者だった自分は SameSite属性 == クロスオリジンでCookieやりとりするときはNoneにしないといけないと誤った理解をしてしまっていました。
なのでローカルもAWS環境もクロスオリジンだったのに、なぜAWS環境だけログアウトできないのかこの時点ではまだわかっていません 笑

SameSite属性とSecure属性ってなんぞや

Cookieには発行する際にいくつか属性を付与できます。(以下一部抜粋

名前 解説
Name Cookieの名前
Value Cookieの値
Expires 有効期限(日)
MaxAge 期限までの秒数
HttpOnly 付与するとJavaScriptからアクセスできなくなる
Secure 付与すると httpsの通信でのみ送信する
SameSite cross-site(クロスサイト)の通信でもCookie送信します?の 設定

解説にも書きましたが、今回取り上げるSecure属性は付与することによりそのCookiehttps通信でなければ、ブラウザ, サーバ間で送信されなくなります。
SameSite属性は、このあと書きますが設定によってブラウザ, サーバー間のCookie送信をクロスサイトでも行うかの設定ができます。

SameSite属性に設定できる値

名前 効果 詳細
Strict 同一サイトでのみCookie送信 セキュリティ強
Lax クロスサイトの場合HTTPメソッドがGETなど特定の条件下でのみCookie送信 Chromeでは未指定だとこれになる。 セキュリティ中
None クロスサイト、同一サイト関係なく送信する Secure属性が必須。セキュリティ弱

今回だと、フロント側のURLとサーバー側のURLの関係がクロスサイトかどうかが重要で自分はここを理解してませんでした。そして当初の理解のSameSite属性 == クロスオリジンでCookieやりとりするときはNoneにしないといけない ではなく SameSite属性 == クロスサイトでCookieやりとりするときはNoneにしないといけない と知りました。(そもそも属性の名前が SameSite(同一サイト)になってるやろ!って話ですね)

ちなみにcross-site(クロスサイト)かsame-site(同一サイト)かの定義は以下の通りです。

スクリーンショット 2022-04-28 2.30.42.png (306.7 kB)

引用:https://zenn.dev/agektmr/articles/f8dcd345a88c97

トップレベルドメイン+セカンドレベルドメインの組み合わせが違うかどうかのようです。

今回の状況

環境 フロント バック 詳細
ローカル http://localhost:3000 http://localhost:8000 same-site なのでCookie送信できる
AWS https://hogehoge.amplifyapp.com/ https://fugafuga.com/ cross-site & SameSite属性が 未指定(Lax) & リクエストメソッドがPOST なのでCookie送信できない

ローカル環境ではポート番号が違うだけで同一サイトになるため、SameSite属性が未指定(Lax)でも問題なくCookieを送信できる。
AWS環境では、まだ載せたばかりだったこともありドメインも違うのでクロスサイトになり、SameSite属性が 未指定(Lax)のログアウト処理だけうまく動かなかったわけです。

http通信のローカル環境でなぜログインできてたのか

SameSite属性は完全に理解したところですが、もう一つ疑問が。
そう。Secure属性です。Secure属性は付与すると、httpsでしかCookie送信しないはず...
↓もう一度その時のソースコード

// ログイン時のcookie
cookie := http.Cookie{Name: name, Value: token, Expires: time.Now().Add(3 * time.Hour), HttpOnly: true, Secure: true, SameSite: http.SameSiteNoneMode}  // ローカル環境のhttp通信では送信しないはず...

// ログアウト時のcookie
cookie := http.Cookie{Name: name, Value: "", HttpOnly: true, MaxAge: -1}

localhost は例外

ブラウザによっては localhost だと httpsの要件が無視されるとのことでした。
※自分はChromeを使ってます

スクリーンショット 2022-04-08 11.43.38(2).png (90.5 kB)

引用:https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Set-Cookie#sect4

開発者のこと考えてってことだよね。ありがてぇ... 笑

まとめ

  • ローカル環境では同じlocalhostでsame-siteのため SameSite属性が未指定(Lax)でもCookie送信できた
  • cross-siteでCookie 送信する場合は、SameSite属性をNone、Secure属性をtrueにするべし
  • Secure属性は localhost だと 無視される(ブラウザによる)

最後に

なんだかんだで今では、AWS環境でフロント側とサーバー側で同じドメインを使い、SameSite属性をStrictにして動かしています。
有名なサーバー攻撃手法のXSSCSRFの対策として、SameSite属性やHttpOnly属性の設定は必須です!!正しい知識で安全にCookie使っていきましょー!!