CookieのSameSite=Laxデフォルト化 アクセスログで影響調査
こんにちは。インフラ担当のいせです。
2月4日リリース予定のChrome80からCookieのSameSite
属性が明示されていない場合の挙動がLax
に変更される予定です。
Cookieは至るとこで使用されており、影響範囲の特定に苦労されている方も多いのではないでしょうか?
一方でChromeにはFetch Metadataリクエストヘッダという機能が実装されています。
このヘッダはリクエストが生成されたページとのオリジン間の関係をサーバ側に通知することができます。
今回はFetch MetadataリクエストヘッダをWebサーバのアクセスログに出力することで、CookieのSameSite
属性のデフォルトがLax
になった場合の影響を調査する方法を解説します。
用語の定義
この記事の中ではURLの構成要素をWHATWGの仕様に従って以下のように定義しています。
- Scheme
- プロトコルを指定します。
- Host
- FQDNやIPアドレスなど、基本的にはネットワーク上の識別名を指定します。
- Registrable Domain
- Domainのうち、Public Suffix(
.com
や.ne.jp
など)とその直前までを指します。 - 通常は個人や組織が勝手に使用することができず、レジストラに登録する必要がある部分です。
- Domainのうち、Public Suffix(
- Port
- ポート番号を指定します。
SameSite=Lax化の影響
CookieのSameSite
属性は、リクエスト生成元のページとのRegistrable Domainの関係によってCookieを送るかどうか制御する属性です。
例えば、www.raccoon.ne.jp
のページ内のiframeタグでwww.superdelivery.com
というサイトを表示した場合、Registrable Domainが異なるページからリクエストが生成されたということになります。
このときSameSite
属性がNone
となっているとwww.superdelivery.com
のCookieが送信されますが、Lax
にするとCookieは送信されません。
これまではSameSite
属性が明示されていない場合はNone
として扱われていましたが、Chrome 80からはLaxに変わります。
SameSite=Lax
の場合、CSRFやClick Jackingといった攻撃への耐性が高まるというメリットがあります。
一方でこの変更により別サイトから遷移してきた際に、ログインのやり直しが必要になるなどの影響が考えられます。
Registrable Domainが異なるサイトから生成されたリクエストにCookieが付与されるかどうかは、SameSite
属性の値によって次のように変化します。
リクエスト生成元 | SameSite=Lax | SameSite=None |
---|---|---|
アンカータグ | ○ | ○ |
imgタグ | × | ○ |
iframeタグ | × | ○ |
formタグ(GET) | ○ | ○ |
formタグ(POST) | × | ○ |
XMLHttpRequest | × | ○ |
Fetch API | × | ○ |
したがってSameSite=Lax
化の影響を調べるには、「アンカータグとGETのfrom以外で、異なるRegistrable Domainから生成されたリクエストでCookieを使用していないか」ということを確認する必要があります。
しかしこれを漏れなく確認していくことは、なかなか骨の折れる作業ではないでしょうか。
アクセスログのReferer
ヘッダを見れば、異なるRegistrable Domainから送られてきたリクエストかどうか、ある程度わかりそうな気もしますが、Referer
ヘッダは送られないケースが多々あったり、そもそもリクエスト生成元のタグ名を判別できなかったりと完璧ではありません。
Fetch Metadataリクエストヘッダとは
Fetch Metadataリクエストヘッダとは、一言で言うと「リクエストが生成されたコンテキストをサーバ側に通知するためのヘッダ」です。
ざっくりとわかりやすく例えるなら、「Referer
ヘッダの進化系」と言ってしまっていいと思います。
例えば、「同じRegistrable Domain内のページからしか生成されないはずのリクエストが、異なるRegistrable Domainから送られてきた」、「imgタグから画像ではないURLがリクエストされている」などといったことをサーバ側で把握することができます。
Referer
ヘッダではプライバシーの問題があり、ブラウザ側でOFFにされてしまうケースがありますが、このヘッダはリクエスト生成元のURLは含まれないため、そのような問題は起こりにくくなっています。
仕様としてはまだドラフトの段階ですが、Chromeでは2019年7月リリースのバージョン76から一部の機能が実装されています。
※以下の説明は本記事執筆時(2020年1月24日)の情報を参照しています。仕様はドラフト段階なので変更になる可能性があります。
この仕様では以下の4つのリクエストヘッダが定義されています。
- Sec-Fetch-Site
- Sec-Fetch-Mode
- Sec-Fetch-Dest
- Sec-Fetch-User
Sec-Fetch-Dest
ヘッダ以外はChromeに実装されています。
今回のSameSite=Lax
化の調査で使用するのはSec-Fetch-Site
とSec-Fetch-Mode
の2つです。
Sec-Fetch-Site
このヘッダはリクエスト生成元とリクエスト先のオリジン間の関係をサーバに通知するものです。
オリジン間の関係には主に以下の3種類があります。
- Same Origin
- Scheme・Host・Portのすべてが一致しているとき
- Same Site
- SchemeとRegistrable Domainが一致しているとき
- Cross Origin
- それ以外の場合
Same Siteという言葉はCookieのSameSite
属性と同じように見えますが、CookieにおけるSame Siteは少し意味が異なります。
CookieにおけるSame SiteはRegistrable Domainが一致しているかどうかだけを見ており、Schemeは含まれません。
例えば、www.raccoon.ne.jp
のページ内のリンクをクリックして、www.superdelivery.com
へ遷移した場合、このヘッダの値はcross-site
となります。
このヘッダが取りうる値は以下のとおりです。
cross-site
- リクエストとリクエスト生成元のオリジンがCross Originの関係にあるとき
same-origin
- リクエストとリクエスト生成元のオリジンがSame Originの関係にあるとき
same-site
- リクエストとリクエスト生成元のオリジンがSame Siteの関係にあるとき
none
- URLをブラウザに入力したりブックマークをクリックするなど、ユーザーの操作から直接的に送信されたリクエスト
Sec-Fetch-Mode
このヘッダはクライアントがリクエストを送信するときのModeをサーバに通知するものです。
HTTPリクエストを送信する機能を持つHTMLタグやJavaScriptのオブジェクトは、Modeと呼ばれる属性を持ちます。
リクエスト送信先がCross Originであるときに「リクエストを送ることができるか」や「JavaScriptからレスポンスにアクセス可能か」がこのModeによって決定されます。
HTMLタグやJavaScriptのオブジェクトはそれぞれ固有のデフォルト値がModeとして設定されていますが、ものによっては下記のように変更することが可能です。
// JavaScriptのFetch APIはデフォルトは`cors`だが`no-cors`に変更
fetch("https://fm-test.paid.jp/test.php?fetch",{
method: 'GET',
mode: 'no-cors'});
Modeは、ブラウザがHTTPの送受信を行うときの動作を標準化したFetch Standardという仕様に定義されています。
このヘッダが取りうる値は以下のとおりです。
cors
- CORSのルールに準拠していれば、Cross Originへのリクエストの送信が可能です。
- リクエスト送受信の過程で
Access-Control-Allow-*
ヘッダがないなどの違反があればエラーになります。
- リクエスト送受信の過程で
- JavaScriptでレスポンスを読み取ることができます。
- Fetch APIなどのデフォルトのModeです。
no-cors
- メソッドとヘッダが以下の場合のみ、Cross Originへのリクエストの送信が可能です。
- 許可されているメソッド:
GET
/HEAD
/POST
- 許可されているヘッダ:
accept
/accept-language
/content-language
/content-type
- 許可されているメソッド:
- JavaScriptでレスポンスを読み取ることはできません(ブラウザが画像などを画面に表示することはできる)。
- imgタグなどのデフォルトのModeです。
same-origin
- このモードのときにはCross Originへリクエストを送ることはできません。
- 例えばJavaScriptのFetch APIはデフォルトのmodeが
cors
ですが、開発者が意図的にsame originにのみリクエストを送信することを強調したいときなどに使われます。
navigate
- ページが遷移する場合はこのモードになります。
- formタグやアンカータグでページ遷移する際にこのモードになります。
nested-navigate
- iframeタグにて送信されるリクエストはこの値がヘッダに指定されます。
- ただし、実はこのモードだけはFetch Standardに定義されていません(iframeタグは本来
navigate
モードで動いています)。このモードはFetch Metadataリクエストヘッダの仕様の中で定義されているもので、iframeヘッダを区別するために使用されています。
websocket
- websocketにて送信されるリクエストはこのモードです。
以下の2つのヘッダについては、今回の調査では使用しないので簡単に説明させていただきます。
Sec-Fetch-Dest
このヘッダはリクエストを生成した要素の種類をサーバに通知するものです。
例えば、imgタグから生成されたリクエストの場合、このヘッダの値はimage
となります。
このヘッダが取りうる値はたくさんあるので、いくつかよく使われそうなものを以下に取り上げます。
image
- imgタグなどから生成されたリクエスト
font
- CSSのフォントとして読み込まれたリクエスト
document
- ページ遷移やiframeタグから生成されたリクエスト
Sec-Fetch-User
ユーザーが明示的に実行した操作から生成されたリクエストかどうかをサーバに通知するものです。
URLを直接入力したり、ブックマークをクリックした場合にTrueになります。
Trueのときは?1
でFalseの場合はヘッダがつきません。
アクセスログから調査する方法
ここからはWebサーバのアクセスログでCookieのSameSite
属性がLax
になったときの影響を調査していきます。
今回はWebサーバとしてApache 2.4.26、ブラウザはChrome 79.0を使用しました。
Apacheの設定
アクセスログにFetch Metadataリクエストヘッダを出力するための設定を追加します。
LogFormat "REQUEST=%r\tRESPONSE_CODE=%s\tREFERER=%{Referer}i\tSEC_FETCH_SITE=%{Sec-Fetch-Site}i\tSEC_FETCH_USER=%{Sec-Fetch-User}i\tSEC_FETCH_MODE=%{Sec-Fetch-Mode}i\tCOOKIE=%{Cookie}i" fetch_metadata_log
CustomLog /var/log/httpd/cross_origin_accsess.log fetch_metadata_log
上記のログ設定は今回の説明で必要な最低限の設定のみとなっています。
送信元IPアドレスや時刻なども出力されないので、必要に応じて拡張して使用してください。
テストページ
検証のため、以下のようなページをhttps://fm-test.superdelivery.com/test.html
を用意しました。
※ここで登場するURLは検証のためのサンプルで、実際には公開されていません。
<html>
<head>
<meta charset="UTF-8">
<title>test page</title>
</head>
<body>
<h1>fm-test.superdelivery.com</h1>
<script type="text/javascript">
var xhr = new XMLHttpRequest();
xhr.open("GET", "https://fm-test.paid.jp/test.php?xhr");
xhr.withCredentials = true
xhr.send();
fetch("https://fm-test.paid.jp/test.php?fetch",{
method: 'GET',
mode: 'no-cors',
credentials: 'include'});
</script>
<a href="https://fm-test.paid.jp/test.php?link">link</a>
<form method="GET" action="https://fm-test.paid.jp/test.php">
<input type="hidden" name="get">
<input type="submit" value="GETを送信">
</form>
<form method="POST" action="https://fm-test.paid.jp/test.php?post">
<input type="submit" value="POSTを送信">
</form>
<img src="https://fm-test.paid.jp/test.php?image">
<iframe src="https://fm-test.paid.jp/test.php?iframe">
</iframe>
</body>
</html>
上記のページから以下のリクエストを送信することができます。
- XMLHttpRequest
- Fetch API
- アンカータグ
- fromタグ(GET)
- formタグ(POST)
- imgタグ
- iframeタグ
また、ログを見たときにわかりやすいようにクエリ文字列を設定しています。
リクエスト送信先のhttps://fm-test.paid.jp/test.php
は特に何もせず200 OK
を返すだけのページです。
検証に当たって、予めChromeにはfm-test.paid.jp
の以下のようなCookieを食わせておきます。
Set-Cookie: lax=value;SameSite=Lax
Set-Cookie: none=value;SameSite=none
アクセスログの結果
上記のページから生成されたリクエストのアクセスログが以下になります。
# Fetch API
REQUEST=GET /test.php?fetch HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=- SEC_FETCH_MODE=no-cors COOKIE=none=value
# fromタグ(GET)
REQUEST=GET /test.php?get= HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=?1 SEC_FETCH_MODE=navigate COOKIE=lax=value; none=value
# formタグ(POST)
REQUEST=POST /test.php?post HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=?1 SEC_FETCH_MODE=navigate COOKIE=none=value
# iframeタグ
REQUEST=GET /test.php?iframe HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=- SEC_FETCH_MODE=nested-navigate COOKIE=none=value
# imgタグ
REQUEST=GET /test.php?image HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=- SEC_FETCH_MODE=no-cors COOKIE=none=value
# アンカータグ
REQUEST=GET /test.php?link HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=?1 SEC_FETCH_MODE=navigate COOKIE=lax=value; none=value
# XMLHttpRequest
REQUEST=GET /test.php?xhr HTTP/1.1 RESPONSE_CODE=200 REFERER=https://fm-test.superdelivery.com/test.html SEC_FETCH_SITE=cross-site SEC_FETCH_USER=- SEC_FETCH_MODE=cors COOKIE=none=value
SameSite=Laxの影響
想定通りSameSite=Lax
の場合は、GETメソッドのformタグとアンカータグのリンク以外から生成されたリクエストでCookieが付与されなくなってしまいました。
しかし、XMLHttpRequestやFetch APIでは明示的にCookie送信を指定しているにもかかわらず、SameSite=None
だと落とされてしまうのはちょっと驚きました。
Fetch Metadataリクエストヘッダから影響を確認
ログを見ると以下の条件に当てはまらないリクエストがSameSite=Lax
の影響を受けることがわかります。
- SEC_FETCH_MODE=navigate
- メソッドがGET
また、Sec-Fetch-Site
がcross-site
以外の値の場合はオリジンが同じかtop-level navigationのどちらかになりますので、SameSite=Lax
の影響を受けません。
したがって以下のような条件でログを調査することで、SameSite=Lax
がデフォルト化されたときにCookieが送信されなくなるものを事前に発見することができそうです。
Sec-Fetch-Site
がcross-site
かつSec-Fetch-Mode
がnavigate
であるGETリクエスト以外でSameSite
属性が明示されていないCookie
まとめ
今回はCookieのSameSite-Lax
化の影響をWebサーバのアクセスログから調査する方法についてまとめました。
弊社が運営しているサイトでは幸い影響がでるものはなさそうでしたが、事前に確認できたので安心してChrome80のリリースを迎えることができそうです。