一覧WB-05-2 · Cookieとブラウザ内保存

HttpOnly・Secure・SameSiteの役割

まずHttpOnly付きCookieがdocument.cookieから見えず、HTTP要求には付くことを確認します。続いてHTTPSのshop.example.testを例に、明示したSameSite属性・遷移方法・HTTPメソッドを送信条件の表へ当てはめます。最後にFetchのcredentialsによる送信制御を確認します。

説明

Cookieの属性はそれぞれ異なる条件を指定します。HttpOnlyはスクリプトからの読み書き、Secureは通信方式、SameSiteはサイトをまたぐ要求での送信に関わります。一つを付けるだけで他の条件まで保護されるわけではありません。

まずHttpOnly付きCookieがdocument.cookieから見えず、HTTP要求には付くことを確認します。続いてHTTPSのshop.example.testを例に、明示したSameSite属性・遷移方法・HTTPメソッドを送信条件の表へ当てはめます。最後にFetchのcredentialsによる送信制御を確認します。

1HttpOnly付きCookieを保存

配信元からwb05-sidとwb05-themeを受け取ります。sidにはHttpOnly、両方にSameSite=Laxが指定されています。

await fetch("/browser-setup")Cookieを設定する応答を受信.then(r => ({status:r.status, setCookie:r.headers.get("Set-Cookie")スクリプトへの公開可否}))

応答は200ですが、スクリプトが取得するSet-Cookieヘッダーはnullです。Set-CookieはFetchでスクリプトへ公開されない応答ヘッダーで、ブラウザ内部ではCookie保存の処理が行われます。

2document.cookieで見える値

Cookie文字列からこの演習の名前だけを取り出します。

document.cookieスクリプトから参照できるCookie.split("; ").filter(x => x.startsWith("wb05-")).sort()表示順をそろえる

wb05-themeだけが見え、HttpOnlyのwb05-sidは含まれません。HttpOnlyはスクリプトからのCookie値の読み取りを防ぎます。保存されていないという意味ではありません。

3HTTP要求にはHttpOnlyも付く

同じオリジンへFetchし、配信側が受け取ったCookieを取得します。

await fetch("/echo")同じオリジンへの要求.then(r => r.json()).then(r => r.cookie配信側が受け取ったCookie.split("; ").filter(x => x.startsWith("wb05-")).sort())

sidとthemeの両方が届きます。HttpOnlyはHTTP要求への自動添付を止めません。このため、注入されたスクリプトが利用者の権限で要求を送る危険まで、HttpOnlyだけで防げるわけではありません。

4HttpOnly Cookieをスクリプトから上書き

同じ名前とパスでsidを書き換えようとしてから、配信側に届いた値を確認します。

(document.cookie = "wb05-sid=changed; Path=/"同じ範囲への書換えを試す, await fetch("/echo")HTTPで届く値を確認.then(r => r.json()).then(r => r.cookie.split("; ").find(x => x.startsWith("wb05-sid="))))

sidはdemo-sessionのままです。同じ範囲のHttpOnly Cookieを、document.cookieから上書きする操作は受け入れられません。名前が似た別パスのCookieなどを作ることまで一律に禁止する属性ではありません。

5通常のCookieは書換えられる

HttpOnlyのないthemeをlightへ変更します。

(document.cookie = "wb05-theme=light; Path=/; SameSite=Lax"通常Cookieを変更, document.cookie.split("; ").filter(x => x.startsWith("wb05-")).sort())

themeはlightへ変わります。スクリプトでHttpOnlyを付けて保護することはできず、HttpOnlyが必要なCookieはサーバーのSet-Cookieで設定します。

6送信条件の表で使う三つのCookie

HTTPSのshop.example.testへ送るCookieを、SameSiteの値だけ変えて用意します。すべてSecure・HttpOnly付きです。

cookiesStrict・Lax・Noneの属性一覧

strict、lax、noneは条件を比較するための名前です。以後のcookieSelectionは、明示されたSameSiteとSecureの条件を計算します。保存済みで、Domain・Path・有効期限は要求に適合するものとして扱います。

7同じサイト内の別オリジン

shop.example.testの文書からapi.example.testへHTTPSのGETを送る条件です。

cookieSelection({url:"https://api.example.test/data"要求先,top:"https://shop.example.test/"起点のサイト,method:"GET",navigation:false})

三つともSameSiteの条件を満たします。二つのホストは別オリジンですが、ここでは同じHTTPSのexample.testというサイトです。SameSiteはオリジンの一致だけで決まる属性ではありません。

8別サイトからの取得要求

other.testを起点としたFetchなど、最上位のページ遷移ではないGETを計算します。

cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/"別サイトを起点にする,method:"GET",navigation:false最上位のページ遷移ではない})

StrictとLaxは送信対象にならず、Noneだけが条件を満たします。SameSite=Noneでも第三者Cookie制限など別のブラウザ設定によって送信されない場合があります。Noneがあらゆる要求での送信を保証するわけではありません。

9リンクをたどる最上位のGET遷移

同じ別サイト起点でも、リンクをたどる最上位のGET遷移に変えます。

cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/",method:"GET",navigation:true最上位のGET遷移})

LaxとNoneが条件を満たします。明示的なLaxでは、サイトをまたぐ場合でも安全なメソッドによる最上位遷移を許します。Strictはこのサイト間遷移では送信しません。

10最上位遷移でもPOSTは別

メソッドだけをPOSTへ変えます。

cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/",method:"POST"安全なメソッドではないPOST,navigation:true})

明示したLaxではPOSTは条件を満たさず、Noneだけになります。SameSiteを省略したCookieへのブラウザ独自の緩和条件と、明示したLaxの条件を混同しないようにします。

11HTTPへ変わる場合のSecure

要求先をHTTPSからHTTPへ変えます。

cookieSelection({url:"http://shop.example.test/data"暗号化されないHTTPの宛先,top:"https://shop.example.test/",method:"GET",navigation:true})

この例のCookieはすべてSecure付きなのでHTTPへ送りません。SecureはCookieの送信経路を制限する属性で、JavaScriptから読めるかはHttpOnlyの担当です。また、サイトの判定にはschemeも含まれます。

12Noneの保存に必要なSecure

SameSite=NoneにSecureがない場合とある場合を、保存条件へ当てはめます。

[cookieAccepted({sameSite:"None",secure:falseSecureなし}),cookieAccepted({sameSite:"None",secure:trueSecure付き})]

Secureなしはfalse、付きはtrueです。これはSameSite=NoneとSecureの組合せについての条件で、その他の保存制限をすべて検査した結果ではありません。

13FetchでCookieを送らない指定

同じオリジンへの要求でもcredentials: omitを指定します。

await fetch("/echo",{credentials:"omit"資格情報を含めない}).then(r => r.json()).then(r => r.cookie)

届いたCookieは空です。Cookie自身の属性に加えて、Fetchの資格情報モードも送信に関わります。HttpOnlyはこの指定を上書きしません。

まとめ

HttpOnlyはスクリプトからの参照・変更を制限しますが、HTTP要求への添付は止めません。

SecureとSameSiteは通信方式やサイト間要求の条件です。役割と判定対象を分けて考えます。

Cookieの属性だけでなく、Fetchのcredentialsやブラウザの保存・第三者Cookie制限も影響します。

操作履歴と解説を振り返れます。

ブラウザ / JavaScript

1
await fetch("/browser-setup").then(r => ({status:r.status, setCookie:r.headers.get("Set-Cookie")}))
{
  "status": 200,
  "setCookie": null
}
2
document.cookie.split("; ").filter(x => x.startsWith("wb05-")).sort()
[
  "wb05-theme=dark"
]
3
await fetch("/echo").then(r => r.json()).then(r => r.cookie.split("; ").filter(x => x.startsWith("wb05-")).sort())
[
  "wb05-sid=demo-session",
  "wb05-theme=dark"
]
4
(document.cookie = "wb05-sid=changed; Path=/", await fetch("/echo").then(r => r.json()).then(r => r.cookie.split("; ").find(x => x.startsWith("wb05-sid="))))
"wb05-sid=demo-session"
5
(document.cookie = "wb05-theme=light; Path=/; SameSite=Lax", document.cookie.split("; ").filter(x => x.startsWith("wb05-")).sort())
[
  "wb05-theme=light"
]
6
cookies
[
  {
    "name": "strict",
    "sameSite": "Strict",
    "secure": true,
    "httpOnly": true
  },
  {
    "name": "lax",
    "sameSite": "Lax",
    "secure": true,
    "httpOnly": true
  },
  {
    "name": "none",
    "sameSite": "None",
    "secure": true,
    "httpOnly": true
  }
]
7
cookieSelection({url:"https://api.example.test/data",top:"https://shop.example.test/",method:"GET",navigation:false})
[
  {
    "name": "strict",
    "send": true
  },
  {
    "name": "lax",
    "send": true
  },
  {
    "name": "none",
    "send": true
  }
]
8
cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/",method:"GET",navigation:false})
[
  {
    "name": "strict",
    "send": false
  },
  {
    "name": "lax",
    "send": false
  },
  {
    "name": "none",
    "send": true
  }
]
9
cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/",method:"GET",navigation:true})
[
  {
    "name": "strict",
    "send": false
  },
  {
    "name": "lax",
    "send": true
  },
  {
    "name": "none",
    "send": true
  }
]
10
cookieSelection({url:"https://shop.example.test/data",top:"https://other.test/",method:"POST",navigation:true})
[
  {
    "name": "strict",
    "send": false
  },
  {
    "name": "lax",
    "send": false
  },
  {
    "name": "none",
    "send": true
  }
]
11
cookieSelection({url:"http://shop.example.test/data",top:"https://shop.example.test/",method:"GET",navigation:true})
[
  {
    "name": "strict",
    "send": false
  },
  {
    "name": "lax",
    "send": false
  },
  {
    "name": "none",
    "send": false
  }
]
12
[cookieAccepted({sameSite:"None",secure:false}),cookieAccepted({sameSite:"None",secure:true})]
[
  false,
  true
]
13
await fetch("/echo",{credentials:"omit"}).then(r => r.json()).then(r => r.cookie)
""

演習完了