RACCOON TECH BLOG

株式会社ラクーンホールディングス 技術戦略部より、
tipsやノウハウなど技術的な話題を
発信いたします。

CookieのSameSite=Laxデフォルト化 アクセスログで影響調査

こんにちは。インフラ担当のいせです。
2月4日リリース予定のChrome80からCookieのSameSite属性が明示されていない場合の挙動がLaxに変更される予定です。
Cookieは至るとこで使用されており、影響範囲の特定に苦労されている方も多いのではないでしょうか?

一方でChromeにはFetch Metadataリクエストヘッダという機能が実装されています。
このヘッダはリクエストが生成されたページとのオリジン間の関係をサーバ側に通知することができます。

今回はFetch MetadataリクエストヘッダをWebサーバのアクセスログに出力することで、CookieのSameSite属性のデフォルトがLaxになった場合の影響を調査する方法を解説します。

用語の定義

この記事の中ではURLの構成要素をWHATWGの仕様に従って以下のように定義しています。

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-Destヘッダ以外はChromeに実装されています。
今回のSameSite=Lax化の調査で使用するのはSec-Fetch-SiteSec-Fetch-Modeの2つです。

Sec-Fetch-Site

このヘッダはリクエスト生成元とリクエスト先のオリジン間の関係をサーバに通知するものです。
オリジン間の関係には主に以下の3種類があります。

Same Siteという言葉はCookieのSameSite属性と同じように見えますが、CookieにおけるSame Siteは少し意味が異なります。
CookieにおけるSame SiteはRegistrable Domainが一致しているかどうかだけを見ており、Schemeは含まれません。

例えば、www.raccoon.ne.jpのページ内のリンクをクリックして、www.superdelivery.comへ遷移した場合、このヘッダの値はcross-siteとなります。

このヘッダが取りうる値は以下のとおりです。

cross-site

same-origin

same-site

none

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

no-cors

same-origin

navigate

nested-navigate

websocket

以下の2つのヘッダについては、今回の調査では使用しないので簡単に説明させていただきます。

Sec-Fetch-Dest

このヘッダはリクエストを生成した要素の種類をサーバに通知するものです。
例えば、imgタグから生成されたリクエストの場合、このヘッダの値はimageとなります。

このヘッダが取りうる値はたくさんあるので、いくつかよく使われそうなものを以下に取り上げます。

image

font

document

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>

上記のページから以下のリクエストを送信することができます。

また、ログを見たときにわかりやすいようにクエリ文字列を設定しています。
リクエスト送信先の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-Sitecross-site以外の値の場合はオリジンが同じかtop-level navigationのどちらかになりますので、SameSite=Laxの影響を受けません。

したがって以下のような条件でログを調査することで、SameSite=Laxがデフォルト化されたときにCookieが送信されなくなるものを事前に発見することができそうです。

まとめ

今回はCookieのSameSite-Lax化の影響をWebサーバのアクセスログから調査する方法についてまとめました。

弊社が運営しているサイトでは幸い影響がでるものはなさそうでしたが、事前に確認できたので安心してChrome80のリリースを迎えることができそうです。

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc