30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

SameSite 属性を使った Cookie のセキュアな運用を考える

Cookie に対しては「属性」というものを設定することができる。そして属性の設定内容によって、Cookie の生存期間を指定したり、送付先の制限を行ったりすることが可能になっている。属性のひとつであるSameSiteは、正しく使うことでセキュリティ対策やプライバシー保護に大きな効果を発揮する機能である。

ただ他の属性に比べると若干複雑で、しかも比較的新しい属性ということもあって、今のところそれほど普及していない。
しかし有用な機能であることは間違いないし、後述するように一部のブラウザベンダはSameSiteの利用を促す方針を明確にしている。
今後はSameSite属性を積極的に活用していくべきだろう。

この記事では、SameSite属性を設定した Cookie がどのように動作するのかを見ていき、その運用方法について考えていく。

Cookie の基本的な仕組みや機能については、こちらを参照。
numb86-tech.hatenablog.com

Cookie の課題

Cookie の仕様はRFC 6265というドキュメントで定められているが、SameSite属性については記述されていない。
SameSiteはまだドラフトで、仕様を策定している段階だからだ。
関連するドラフトはいくつかあるのだが、ここではRFC 6265bisの内容を参照しながら話を進めていく。
これはRFC 6265の改訂版のドラフトで、SameSiteについても記述してある。

Cookie の特徴は、対象となるドメインへの HTTP リクエストに自動的に付与されることである。明示する必要なく、予めサーバから渡しておいたデータをリクエストに含めてくれる。
これが大きな利便性を生む反面、問題も生み出してきた。
リクエストが発生してしまえば、それがユーザーの意図したものであろうとなかろうと、Cookie も送信されてしまう。この仕組みを悪用されることで、ユーザーが意図しない操作(SNS への不適切な投稿やパスワードの変更など)が行われたり、個人情報が不当に収集されたりしてきた。

Cookie の仕組みを悪用した攻撃に対してウェブアプリケーション側も当然対策を行っており、その知見がネットや書籍で共有されている。だがそれらはどうしても、対症療法的なものにならざるを得なかった。
「ユーザーが意図しない形での Cookie の送信」が発生することを前提とし、そのリクエストの正当性をチェックするというのが基本的なアプローチになる。具体的な方法はここでは説明しないが、トークンの付与やプリフライトリクエストの利用などがよく使われる。プリフライトリクエストによる対策については以前ブログに書いた。

numb86-tech.hatenablog.com

これに対して、「ユーザーが意図しない形での Cookie の送信」をそもそも発生させないようにする、というのがSameSite属性の特徴である。
SameSiteを設定するだけであらゆる攻撃を防げるわけではもちろんないが、適切に使えばそれだけで大きな効果を得ることができる。

SameSite 属性に設定できる値

現在、SameSite属性に設定できる値は以下の 3 つ。

  • Strict
  • Lax
  • None

指定方法は他の属性と同様にSameSite=設定値なので、例えばStrictを指定する場合は以下のようになる。

key=value; SameSite=Strict

SameSite属性を設定しなかった場合、もしくはStrictLax以外の値を指定した場合は、Noneとみなされる。もちろん明示的にNoneを指定することもできるし、可読性から考えてもそのほうがよいだろう。

だが Google Chrome においては、2020 年 2 月にリリース予定のバージョン80から、この挙動が変わる。
SameSite属性なし、もしくはStrictNone以外の値を設定した場合は、Laxとして扱うようになる。
つまり明示的にNoneを指定した場合にのみNoneとして扱われるようになり、さらにNoneの使用に対して独自の制限を設ける。
バージョン80以降の Google Chrome の方針については、この記事の後半で詳しく説明する。

SameSite属性に設定した値と、リクエストの種類。その組み合わせによって Cookie を付与するかどうか判断するというのが、SameSite属性の機能である。
どこからのリクエストなのか、リクエストメソッドは何か、などを見て、このリクエストについてはNoneが設定された Cookie のみを付与しよう、こっちのリクエストはNoneLaxの Cookie を付与しよう、という具合に判断していく。この機能よって、必要以上に Cookie の送信を行わない、より安全なウェブアプリケーションを構築することが可能になる。

では、具体的にどのようなルールで、Cookie を付与するかどうか判断しているか。
ここからは、そのルールを理解するために重要となるいくつかの概念を説明していく。
遠回りに思えるかもしれないが、まずはこれらの概念を理解しておくことで、実際のブラウザの挙動やコードが理解しやすくなる。

same-site と cross-site

まず最初に理解しておきたいのは、same-sitecross-siteという概念である。
これはリクエストを、それがどこから発生したのかによってsame-siteもしくはcross-siteのいずれかに分類するというもの。
ドメインが同じであればsame-siteであり、ドメインが異なればcross-siteとなる。

例えばhttp://a.com/topにあるリンクを踏んでhttp://a.com/menuに移動する場合、同じa.comというドメインからhttp://a.com/menuに対するリクエストが発生しているため、これはsame-siteである。
http://b.com/topにあるリンクを踏んでhttp://a.com/menuに移動する場合は、b.comという異なるドメインからhttp://a.com/menuに対するリクエストが発生しているので、cross-siteとなる。

オリジンではなくドメインで判断するので、注意する。例えばhttp://a.com/からhttps://a.com/mypageにリクエストが発生する場合は、プロトコルが違うのでオリジンは異なるが、ドメインはどちらもa.comなので、これはsame-siteになる。

アドレスバーに直接 URL を入力することで発生したリクエストも、same-siteとなる。
また、ブラウザ外からページを開いた場合も、same-siteである。例えば、ターミナルで$ open https://a.com/mypageというコマンドを実行すると、ブラウザでhttps://a.com/mypageを開くが、このリクエストもsame-siteとして扱われる。

基本的にはsame-siteからのリクエストは比較的安全であり、SameSite属性もそれを前提に設計されている。
セキュリティやプライバシーの文脈で重要なのはcross-siteからのリクエストであり、そのリクエストに Cookie を含めるかどうかが、論点となる。

safe なメソッド

リクエストが安全なものであるかどうかは、メソッドによってもある程度は判断できる。例えばGETは、対象となるリソースに変更を加えないため、POSTDELETEに比べて安全と言える。

RFC 72314.2.1では、以下の 4 つのメソッドをsafeなメソッドとして定義している。

  • GET
  • HEAD
  • OPTIONS
  • TRACE

RFC 6265bisでも、RFC 7231の内容に準拠して、これらのメソッドをsafeなものとして扱っている(5.3.7.1)。

念の為書いておくと、これらは HTTP のルール上そうなっているという話であり、開発者がこのルールを守らずにGETでリソースの変更を行うようなウェブアプリケーションを作っていれば、そのアプリケーションにおいてはGETは安全とは言えなくなる。
当たり前の話だが、SameSite属性の設定だけを頑張っても、他の部分が雑な作りになっていれば台無しになってしまう。

Top Level Navigation

リクエストがTop Level Navigationであるかも、リクエストの安全性を測る基準のひとつになる。

RFC 6265bisに目を通しても、Top Level Navigationの明確な定義は見当たらなかった。
だが、「アドレスバーに表示されている URL のオリジンのドメインをtop-level siteとする」という記述はあった(5.2.1)。そのため、リクエストした URL がアドレスバーに表示されるような動作が、Top Level Navigationなのだと思われる。
後述する検証結果も、それを裏付けるものだった。

具体的には、http://a.com/にあるリンクを踏んでhttp://b.com/に遷移した場合、アドレスバーにはhttp://b.com/が表示されるので、これはTop Level Navigationである。
http://a.com/内に設置された JavaScript が実行され、そのコードにfetch('http://b.com/')と書かれていた場合、http://b.com/へのリクエストは発生するが、アドレスバーの表示はhttp://a.com/のままである。そのためこのケースは、Top Level Navigationではない。

アドレスバーの表示を基準にするのは、多くのユーザーはアドレスバーに表示されている内容によって、今自分がどのサイトにいるか判断するためである。
一般的なユーザーにとって、そのサイトを信頼するかどうかを判断するために使える唯一の材料が、アドレスバーなのだと言える。

そのため、リクエストした URL とアドレスバーの表示が一致する動作は、一般的なユーザーの直感とも一致している。
逆に、リクエストした URL がアドレスバーに反映されないような動作は、リクエストが行われたことにユーザーが気付かない可能性が高く、意図しない操作が行われてもそれを認識できない恐れがある。

SameSite 属性の基本的なルール

SameSite属性では、ここまで説明してきた概念を使って、リクエストに Cookie を付与するかどうか判定している。
具体的には以下の通り。

Strictの場合、same-siteでは常に付与する。cross-siteでは常に付与しない。
Laxの場合、same-siteでは常に付与する。cross-siteでは、Top Level Navigationかつメソッドがsafeのときにのみ、付与する。
Noneの場合、same-siteでは常に付与する。cross-siteでも常に付与する。

表にまとめると次のようになる。

same-site cross-site
Strict ×
Lax Top Level Navigationかつメソッドがsafeのときのみ◯
None

StrictNoneはシンプルであり、分かりづらいのはcross-site時のLaxだけである。
そのためここからは、cross-site時の実際の挙動を確認していく。

検証のための準備

サーバは Node.js のv12.14.1、ブラウザは以下の 3 つを使って、動作確認した。

  • Google Chrome 79.0.3945.130
  • Safari 13.0.4
  • Firefox 72.0.2

どのブラウザでも同じように動き、ブラウザによる差異はないことを確認した。ただ注意すべき事項もあるので、それについては適宜説明していく。

まず、ローカル環境でサブドメインを使えるようにするため、hosts ファイルを編集する。
Mac の場合は/etc/hostsが hosts ファイルなので、これに以下の内容を追加する。

127.0.0.1       sub.localhost

これで、localhostの他にsub.localhostを使えるようになった。

この検証では、http://localhost:8080/http://sub.localhost:8081/という、2 つのドメインを用意することにする。

続いて、http://localhost:8080/のコードを書く。
このドメインで、ブラウザに Cookie をセットする。

const http = require('http');

http.createServer((req, res) => {
  res.setHeader('Set-Cookie', [
    'strict=value; SameSite=Strict',
    'lax=value; SameSite=Lax',
    'none=value; SameSite=None',
  ]);
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write('<h1>Cookie を付与するドメイン</h1>');
  res.write('<p>');
  res.write('<a href="http://sub.localhost:8081">別ドメインのサイト(sub.localhost:8081)に移動する</a>');
  res.write('</p>');
  res.end();
}).listen(8080);

3 種類の Cookie をセットした。分かりやすいように、SameSite属性の値をそのまま Cookie の名前にしている。
このコードに、以下の内容を追加する。これは、http://sub.localhost:8081/のコード。

const html = `
<html>
<head></head>
<body>
<p>
  <a href="http://localhost:8080/">a タグ</a>
</p>
<p>
  <form action="http://localhost:8080/" method="GET">
    <button type="submit">form による GET メソッドでのリクエストを行う</button>
  </form>
</p>
<p>
  <form action="http://localhost:8080/" method="POST">
    <button type="submit">form による POST メソッドでのリクエストを行う</button>
  </form>
</p>
</body>
</html>
`;

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write(html);
  res.end();
}).listen(8081);

このコードを Node.js で実行してサーバを起動し、http://localhost:8080/にアクセスする。
すると Cookie がセットされるので、動作確認のためにリロードしてリクエストヘッダを確認してみる。

GET / HTTP/1.1
Host: localhost:8080
Cookie: strict=value; lax=value; none=value

これはsame-siteなので 3 つ全て送信されている。なお、本記事とは無関係なフィールドについては省略している。

検証の準備が整ったので、表示されているリンクでhttp://sub.localhost:8081/に移動する。
検証は全て、http://sub.localhost:8081/からhttp://localhost:8080/にリクエストを送ることで行う。
ドメインが異なるためcross-siteになる。そのため、Strictは常に送信されない。

a タグ

まずは「a タグ」という文字をクリックしてaタグによるリクエストを発行し、そのリクエストヘッダを調べる。

GET / HTTP/1.1
Host: localhost:8080
Cookie: lax=value; none=value

メソッドはGETなのでsafeである。
そして、アドレスバーにhttp://localhost:8080/が表示されており、リクエストした URL と一致する。よって、Top Level Navigationである。
そのため、NoneだけでなくLaxも送信される。

フォームの送信

次はformタグによるリクエスト。メソッドはGETPOSTかを選べるが、それによって結果が変わる。

まずは「form による GET メソッドでのリクエストを行う」をクリックしたときのリクエストヘッダ。

GET / HTTP/1.1
Host: localhost:8080
Cookie: lax=value; none=value

aタグのときと同じで、safeなメソッドかつTop Level Navigationなので、LaxNoneが送信される。

次は、「form による POST メソッドでのリクエストを行う」をクリックしたときのリクエストヘッダ。

POST / HTTP/1.1
Host: localhost:8080
Cookie: none=value

こちらもTop Level Navigationではあるのだが、リクエストメソッドがPOSTなのでsafeではない。
そのためNoneのみが送信される。

prerender

次はprerenderによるリクエストを試す。

http://sub.localhost:8081/の html のheadタグのなかに、以下のコードを追記する。

<link rel="prerender" href="http://localhost:8080/?prerender" crossorigin="use-credentials">

prerenderとは、リソースを先読みするための機能。
ブラウザは、hrefで指定されたリソースを予め読み込んでおく。そうすることで、リンクを踏むなどして実際にそのリソースに移動するときに、瞬時にそのページを表示することができる。

そのため、実際に表示するページに対するリクエストの他に、先読みするページに対するリクエストも発生する。
今回の例では、http://localhost:8080/?prerenderを先読みしている。クエリをつけたのは、prerenderによるリクエストであることを分かりやすくするためであり、それ以上の意味はない。

しかし今日現在、Firefox や Safari ではこの機能を使うことはできない。
Can I use... Support tables for HTML5, CSS3, etc

そのため、Google Chrome のみで検証している。
そして、開発者ツールで当該リクエストを確認することはできないため、http://localhost:8080/側のコードを書き換えて、リクエストの内容がログに表示されるようにした。
具体的には、以下のコードを書く。

console.log(req.url, req.method, req.headers.cookie);

この状態でサーバを起動し直してhttp://sub.localhost:8081/にアクセスすると、ログに以下の内容が表示された。

/?prerender GET lax=value; none=value

NoneだけでなくLaxも送信されているので、prerendersafeなメソッドでありTop Level Navigationであるということが分かった。
GETなのでsafeなのは当然だが、なぜTop Level Navigationなのか。先読みをしているだけなので、アドレスバーには依然としてhttp://sub.localhost:8081/が表示されている。

prerenderに関するドキュメントには、prerenderを実行すると新しくページを作り、そこに対象のリソースをレンダリングすると書かれている。そしてそのページは非表示であり、ユーザーには見えない。先読みしたページに移動しようとすると、非表示ページにレンダリングしていた内容が、現在見ているページにスワップされる。
このような仕組みであるため、Top Level Navigationになるのだと思う。ユーザーには見えていないだけで、ブラウザは新しくページを開いており、そこにhrefで指定した URL の内容を表示させている。つまり、非表示のページであるということ以外は、アドレスバーに URL を打ち込んで表示しているのと何も変わらない。

開発者ツールで確認できないのも、同じ理由だと思われる。今見ているページのなかでリクエストが発生しているのではなく、新しくページを作ってそこでリクエストしている。だから今見ているページのNetworkタブには表示されない。

iframe 内でのフォームの送信

次は、iframe内でformタグによる送信を行う。
この手法を使うと、ユーザーに気付かれることなくフォームからの送信を行える。

今回のサンプルでは、以下のような仕組みになっている。
http://sub.localhost:8081/からhttp://sub.localhost:8081/iframeへのリンクを用意する。
http://sub.localhost:8081/iframeにはiframeでフォームが埋め込まれており、フォームを読み込むと同時に送信するようにしてある。
しかしiframe内でフォームの送信を行っているので、画面遷移は行われない。そしてスタイルの設定によってiframe自体を非表示にしている。
そのため、フォームによる送信が行われたことにユーザーが気付く可能性は低い。

以下がそのコードだが、かなり長いので、上記の説明が理解できていれば無理して読む必要はない。

const topPageHtml = `
<html>
<head>
</head>
<body>
<p>
  <a href="http://localhost:8080/">a タグ</a>
</p>
<p>
  <a href="/iframe">iframe 内の form からリクエストを行う</a>
</p>
</body>
</html>
`;

const iframeGetHtml = `
<body onload="document.forms[0].submit()">
  <form action="http://localhost:8080/" method="GET">
  </form>
</body>
`;

const iframePostHtml = `
<body onload="document.forms[0].submit()">
  <form action="http://localhost:8080/" method="POST">
  </form>
</body>
`;

const iframePageHtml = `
<p>
  iframe でリクエストを送りました。
</p>
<p>
  <a href="http://localhost:8080">http://localhost:8080 のトップページに移動</a>
</p>
<iframe width="0" height="0" style="visibility: hidden;" src="/iframe-get"></iframe>
<iframe width="0" height="0" style="visibility: hidden;" src="/iframe-post"></iframe>
`;

http.createServer((req, res) => {
  switch(true) {
    case /^\/$/.test(req.url):
      res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
      res.write(topPageHtml);
      res.end();
      break;
    case /^\/iframe-get$/.test(req.url):
      res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
      res.write(iframeGetHtml);
      res.end();
      break;
    case /^\/iframe-post$/.test(req.url):
      res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
      res.write(iframePostHtml);
      res.end();
      break;
    case /^\/iframe$/.test(req.url):
      res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
      res.write(iframePageHtml);
      res.end();
      break;
    default:
      res.writeHead(404);
      res.end();
  };
}).listen(8081);

GETPOSTそれぞれのフォームを用意しているので、http://sub.localhost:8081/iframeにアクセスすると、http://localhost:8080/に対して以下の 2 つのリクエストが発生する。

GET / HTTP/1.1
Host: localhost:8080
Cookie: none=value
POST / HTTP/1.1
Host: localhost:8080
Cookie: none=value

どちらもNoneしか送信されていない。これは、iframe内でのフォームの送信はTop Level Navigationではないためである。

img タグ

次は、imgタグによるリクエスト。srcでパスを指定すれば、それに対するリクエストが当然発生する。

サブドメイン側のコードを以下のようにして再実行してから、http://sub.localhost:8081/にアクセスする。

const html = `
<html>
<head></head>
<body>
<p>
  <a href="http://localhost:8080/">a タグ</a>
</p>
<img src="http://localhost:8080/">
</body>
</html>
`;

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write(html);
  res.end();
}).listen(8081);

safeなメソッドではあるがTop Level Navigationではないので、Noneのみが送信される。

GET / HTTP/1.1
Host: localhost:8080
Cookie: none=value

fetch を使ったリクエスト

最後は、fetchによるリクエストを検証する。
http://sub.localhost:8081/scriptタグを埋め込み、そのなかでfetchを実行する。

const html = `
<html>
<head></head>
<body>
<p>
  <a href="http://localhost:8080/">a タグ</a>
</p>
<script>
fetch('http://localhost:8080/', {credentials: 'include'});
</script>
</body>
</html>
`;

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write(html);
  res.end();
}).listen(8081);

これもTop Level Navigationではないので、メソッドを問わず、Noneしか送信されない。

GET / HTTP/1.1
Host: localhost:8080
Cookie: none=value

SameSite 属性の動作一覧

ここまで検証してきた内容を、表にまとめる。
cross-siteの検証結果しか書いてこなかったが、same-siteでも同じ検証を行っているので、その結果も記載しておく。same-siteの場合はStrictLaxNone全ての Cookie が常に送信されるので、特に覚えることはないのだが。

以下が、Laxが送信されるかどうかの早見表である。

same-site cross-site
a タグ
form get
form post ×
prerender
iframe 内での form get ×
iframe 内での form post ×
img ×
fetch ×

そして既に書いたことの繰り返しになるが、Strictは、same-siteの場合にのみ常に送信される。cross-siteの場合は常に送信されない。
Noneは、same-siteでもcross-siteでも常に送信され、StrictLax以外の値を指定したり、SameSite属性を設定しなかった Cookie は全て、Noneとして扱われる。
これらは全てRFC 6265bisに書かれている内容であり、今回検証した各ブラウザの挙動もそれに沿ったものだった。

だが 2020 年 2 月リリース予定の Google Chrome 80 は、SameSite属性について独自の変更を加えている。

Google Chrome 80 での挙動

具体的な変更点は以下の 2 つ。

  1. SameSite属性が設定されていない、もしくはStrictNone以外の値が指定されている場合は、Laxとして扱う
  2. Noneを指定した場合はSecure属性の設定も必須になる

つまりこれまではデフォルトがNoneだったのがLaxになり、そしてNoneは HTTPS 通信でしか使えなくなる、ということである。
従来の仕様よりもセキュリティを厳しくしていると言える。

この挙動についても、実際に検証してみた。

まだリリース前の機能だが、アドレスバーにchrome://flags/と入力し、SameSite by default cookiesCookies without SameSite must be secureの項目を有効にすることで、試すことができる。

HTTPS 通信が必須なので、OpenSSL で自己署名証明書を用意、秘密鍵(server.key)と証明書(server.crt)を作成した。

以下がコードの全文。

const https = require('https');
const fs = require('fs');

const options = {
  key : fs.readFileSync('./certificate/server.key'),
  cert: fs.readFileSync('./certificate/server.crt')
};

https.createServer(options, (req, res) => {
  res.setHeader('Set-Cookie', [
    'strict=value; SameSite=Strict',
    'lax=value; SameSite=Lax',
    'noSecureNone=value; SameSite=None',
    'secureNone=value; SameSite=None; Secure',
    'invalid=value; SameSite=Foo',
    'noSpecify=value',
  ]);
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write('<h1>Cookie を付与するドメイン</h1>');
  res.write('<p>');
  res.write('<a href="https://sub.localhost:8081">別ドメインのサイト(sub.localhost:8081)に移動する</a>');
  res.write('</p>');
  res.end();
}).listen(8080);

const html = `
<html>
<head></head>
<body>
<p>
  <a href="https://localhost:8080/">a タグ</a>
</p>
<img src="https://localhost:8080/">
</body>
</html>
`;

https.createServer(options, (req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write(html);
  res.end();
}).listen(8081);

Noneの取り扱いや、SameSiteに無効な値を指定したケース(invalid)、SameSiteを設定しなかったケース(noSpecify)などについて、検証していく。

https://localhost:8080/にアクセスすると、Cookie がセットされる。
リロードして、そのリクエストヘッダを確認してみる。

GET / HTTP/1.1
Host: localhost:8080
Cookie: strict=value; lax=value; secureNone=value; invalid=value; noSpecify=value

same-siteなので全ての Cookie が送信されるはずだが、noSecureNoneだけが送信されていない。
このことから分かるのは、Noneを指定してSecure属性は設定しなかった場合、送信先が制限されるのではなく、いかなるリクエストにも含まれない無効な Cookie になってしまうということ。そのため、Noneを指定する場合は必ずSecure属性も設定しなければならない。

次に、「別ドメインのサイト(sub.localhost:8081)に移動する」をクリックして、https://sub.localhost:8081/に移動する。
すると、imgタグによるリクエストが発生する。これは、「safeなメソッドだがTop Level Navigationではないcross-siteのリクエスト」である。

GET / HTTP/1.1
Host: localhost:8080
Cookie: secureNone=value

次に「a タグ」をクリックして、https://localhost:8080/遷移する。
これは、「safeなメソッドかつTop Level Navigationである、cross-siteのリクエスト」である。

GET / HTTP/1.1
Host: localhost:8080
Cookie: lax=value; secureNone=value; invalid=value; noSpecify=value

以上の結果から、SameSite属性を設定しなかった Cookie(noSpecify)や、StrictNone以外の値を指定した Cookie(invalid)は、Laxになること、そしてNoneを指定する場合はSecure属性が必須であることを、確認できた。

ウェブアプリケーションがこの変更によってどの程度影響を受けるのかは、これまでの Cookie の運用によって異なる。同一ドメインでしか Cookie を受け渡ししておらず、SameSite属性を何も設定していないのなら、影響はほとんどないかもしれない。

だが例えそうであったとしても、Cookie の仕組みを悪用した攻撃に対する非常に本質的な対策になるため、SameSite属性は積極的に使っていきたい機能である。
とはいえよく理解せずに使ってしまうと、ユーザービリティを損ねることになってしまう。

read access と write access

単純に考えれば、Strictが一番厳しいのだから、全ての Cookie をStrictにしてしまうのが最も強固である。
しかしそうすると、セッション管理に Cookie を使っている場合、他のドメインからリンクで遷移してきたときにログインしていないと見做されてしまう。

一番最初の検証で確認したように、他のドメインからaタグで遷移した場合、Strictは送信しない。そのためセッショントークンをStrictで保存していた場合、リクエストにセッショントークンが含まれず、ログイン状態ではないと見做されてしまう。同一ドメイン内で遷移し直したりページをリロードしたりすれば、そのリクエストはsame-siteとなるので、セッショントークンは送信されログイン状態になる。だがこれはさすがに使い勝手が悪い。

この問題への対処法としてRFC 6265bisでは、read access用の Cookie とwrite access用の Cookie に分けて管理することを提案している(8.8.2)。

まず、異なる権限を持った 2 種類の Cookie を用意する。ひとつには、GETなどによるリソースの取得のみを許可する。これがread access。もう一方には、それに加えて、リソースの操作も許可する。これがwrite access
そして前者にはLaxを指定し、後者にはStrictを指定する。
こうすれば、他のドメインから遷移してきてもread accessは送信されるためログイン状態が維持される。それでいて、リソースの操作、例えば SNS への投稿やパスワードの変更といったものについては、同一ドメイン内からリクエストした場合にのみ許可されるため、安全性が高まる。

参考資料