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 の送信」が発生することを前提とし、そのリクエストの正当性をチェックするというのが基本的なアプローチになる。具体的な方法はここでは説明しないが、トークンの付与やプリフライトリクエストの利用などがよく使われる。プリフライトリクエストによる対策については以前ブログに書いた。
これに対して、「ユーザーが意図しない形での Cookie の送信」をそもそも発生させないようにする、というのがSameSite
属性の特徴である。
SameSite
を設定するだけであらゆる攻撃を防げるわけではもちろんないが、適切に使えばそれだけで大きな効果を得ることができる。
SameSite 属性に設定できる値
現在、SameSite
属性に設定できる値は以下の 3 つ。
- Strict
- Lax
- None
指定方法は他の属性と同様にSameSite=設定値
なので、例えばStrict
を指定する場合は以下のようになる。
key=value; SameSite=Strict
SameSite
属性を設定しなかった場合、もしくはStrict
かLax
以外の値を指定した場合は、None
とみなされる。もちろん明示的にNone
を指定することもできるし、可読性から考えてもそのほうがよいだろう。
だが Google Chrome においては、2020 年 2 月にリリース予定のバージョン80
から、この挙動が変わる。
SameSite
属性なし、もしくはStrict
かNone
以外の値を設定した場合は、Lax
として扱うようになる。
つまり明示的にNone
を指定した場合にのみNone
として扱われるようになり、さらにNone
の使用に対して独自の制限を設ける。
バージョン80
以降の Google Chrome の方針については、この記事の後半で詳しく説明する。
SameSite
属性に設定した値と、リクエストの種類。その組み合わせによって Cookie を付与するかどうか判断するというのが、SameSite
属性の機能である。
どこからのリクエストなのか、リクエストメソッドは何か、などを見て、このリクエストについてはNone
が設定された Cookie のみを付与しよう、こっちのリクエストはNone
とLax
の Cookie を付与しよう、という具合に判断していく。この機能よって、必要以上に Cookie の送信を行わない、より安全なウェブアプリケーションを構築することが可能になる。
では、具体的にどのようなルールで、Cookie を付与するかどうか判断しているか。
ここからは、そのルールを理解するために重要となるいくつかの概念を説明していく。
遠回りに思えるかもしれないが、まずはこれらの概念を理解しておくことで、実際のブラウザの挙動やコードが理解しやすくなる。
same-site と cross-site
まず最初に理解しておきたいのは、same-site
とcross-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
は、対象となるリソースに変更を加えないため、POST
やDELETE
に比べて安全と言える。
RFC 7231
の4.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 | ◯ | ◯ |
Strict
とNone
はシンプルであり、分かりづらいのは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
タグによるリクエスト。メソッドはGET
かPOST
かを選べるが、それによって結果が変わる。
まずは「form による GET メソッドでのリクエストを行う」をクリックしたときのリクエストヘッダ。
GET / HTTP/1.1 Host: localhost:8080 Cookie: lax=value; none=value
a
タグのときと同じで、safe
なメソッドかつTop Level Navigation
なので、Lax
とNone
が送信される。
次は、「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
も送信されているので、prerender
はsafe
なメソッドであり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);
GET
とPOST
それぞれのフォームを用意しているので、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
の場合はStrict
、Lax
、None
全ての 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
でも常に送信され、Strict
かLax
以外の値を指定したり、SameSite
属性を設定しなかった Cookie は全て、None
として扱われる。
これらは全てRFC 6265bis
に書かれている内容であり、今回検証した各ブラウザの挙動もそれに沿ったものだった。
だが 2020 年 2 月リリース予定の Google Chrome 80 は、SameSite
属性について独自の変更を加えている。
Google Chrome 80 での挙動
具体的な変更点は以下の 2 つ。
SameSite
属性が設定されていない、もしくはStrict
かNone
以外の値が指定されている場合は、Lax
として扱うNone
を指定した場合はSecure
属性の設定も必須になる
つまりこれまではデフォルトがNone
だったのがLax
になり、そしてNone
は HTTPS 通信でしか使えなくなる、ということである。
従来の仕様よりもセキュリティを厳しくしていると言える。
この挙動についても、実際に検証してみた。
まだリリース前の機能だが、アドレスバーにchrome://flags/
と入力し、SameSite by default cookies
とCookies 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
)や、Strict
やNone
以外の値を指定した 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 への投稿やパスワードの変更といったものについては、同一ドメイン内からリクエストした場合にのみ許可されるため、安全性が高まる。