5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cookie の `__Host-` / `__Secure-` プレフィックスとは? 名前でセキュリティを強制する仕組み

5
Posted at

はじめましての方ははじめまして、そうでない方はこんにちは。社内では「じぇっと」と呼ばれています。なぜそう呼ばれているかは追々お伝えできればいいかなと思います。

「cookie名に __Host- が付いてる」— これは何?

開発中のアプリやブラウザの開発者ツールで他社サイトを覗いていて、__Host-session__Secure-token のように、頭に妙な記号付きの名前が付いた cookie を見かけたことはないでしょうか。最初に見ると「フレームワークが内部的に付けている飾り」「社内の命名規約のようなもの」と思いがちです。実際、ただの名前なら自分で好きに付け替えてもよさそうに見えます。

しかしこれは飾りでも慣習でもありません。__Host- / __Secure- で始まる名前は、ブラウザがその名前を見て受理条件を強制するセキュリティ機構です。条件を満たさない Set-Cookie レスポンスは、ブラウザ側で保存されずに捨てられます。この記事では、cookie の属性をおさらいしたうえで「属性だけでは塞げない穴」を示し、プレフィックスがその穴をどう塞ぐのかを順に見ていきます。

前提:cookieのセキュリティ属性をおさらい

プレフィックスの話に入る前に、cookie に付けられる主なセキュリティ関連属性を整理します。

属性 動作 役割/効果
Secure https など安全な経路でのみ cookie を送信する 平文 HTTP での盗聴・漏洩を防ぐ
HttpOnly JavaScript(document.cookie 等)からの読み取りを禁止する XSS 時の cookie 窃取リスクを下げる
SameSite 別サイトからのリクエストに cookie を付けるかを制御(Lax/Strict/None CSRF(クロスサイトリクエスト)のリスクを下げる
Domain cookie を送る対象ホストの範囲を指定する(指定するとサブドメインにも届く) 送る範囲を決める(防御属性ではない)
Path cookie を送る対象パスを絞る 送る範囲を決める(防御属性ではない)

ここで注意したいのは、DomainPath は「cookie を送る範囲を広げる/絞る」ための属性であって、それ自体は攻撃を防ぐためのものではないという点です。とくに Domain は、指定すると指定ドメインとそのサブドメイン全体に cookie が届くようになり、範囲を広げる働きをします。この「範囲の緩さ」が、次に見る攻撃の穴につながります。

属性だけでは防げない攻撃

cookie の属性は、あくまで「サーバが正しく付ける」前提で成り立っています。逆に言えば、別ホストやサブドメインからの設定、あるいは設定ミスがあると、同じ名前のまま属性だけが緩い cookie を作れてしまう余地が残ります。ここでは条件付きで成立しうる代表的な3つの穴を、攻撃者視点で見ていきます。

1. サブドメインからの上書き/注入

Domain 属性は範囲を広げる方向に働きます。たとえば攻撃者が evil.example.com のような同一ドメイン配下のサブドメインを掌握できた場合、そこから Set-Cookie: session=...; Domain=example.com を返すと、その cookie は本体の example.com にも届きます。本体アプリが参照しているセッション cookie と同じ名前で設定できれば、値の上書きや注入が成立しうる、という構図です(サブドメインを取れる、という前提が必要な条件付きの攻撃です)。

2. cookie 固定化(session fixation)

cookie 固定化は、攻撃者があらかじめ用意したセッションIDを被害者のブラウザに固定させ、被害者がそのIDのままログインしてしまうと、攻撃者も同じIDで認証済みセッションに相乗りできる、という攻撃です。上の「上書き/注入」が成立する経路があると、攻撃者が値を固定する手段になりえます。サーバ側でログイン時にセッションIDを再生成していない、といった条件と組み合わさったときに問題になります。

3. 平文 HTTP 経由での設定

Secure 属性を付け忘れた cookie は、平文 HTTP のリクエストでも送信されます。すると中間者(同じネットワークにいる攻撃者など)が平文 HTTP レスポンスに Set-Cookie を差し込み、cookie を上書きできる余地が生まれます。Secure を付けていても、付け忘れや別経路からの設定が一箇所でもあれば穴になりえます。

これら3つに共通するのは、「名前は同じまま、属性だけが緩い cookie を作れてしまう」ことが根にある点です。アプリが参照する名前は固定なのに、その名前で属性の緩い cookie を別ホストやサブドメイン、別経路から差し込めてしまう。ならば、名前の側に「正しい属性で設定されたこと」を縛り付けられないか——というのが、次に見るプレフィックスの発想です。

解決策:cookie名プレフィックス

属性そのものは、上で見たように別ホストやサブドメインからの設定、あるいは設定ミスで崩れる余地があります。そこで発想を変えて、名前に受理条件を埋め込み、ブラウザが cookie を受け取る瞬間に検査させるのがプレフィックスの考え方です。

動作はシンプルです。cookie の名前が予約されたプレフィックス(__Secure- / __Host-)で始まっているのに、そのプレフィックスが要求する条件を満たしていない場合、ブラウザはその Set-Cookie保存せずに無視します。条件を満たした cookie だけが保存されます。

なぜ「名前」に埋め込むのが効くのでしょうか。攻撃者が条件を回避しようとして __Host- を外した別名(たとえばただの session)で cookie を設定したとしても、それは名前の異なる別の cookie になります。アプリが参照しているのは __Host-session という名前なので、別名の cookie はそこには届きません。「名前を改ざんすると、もはや同じ cookie ではなくなる」——この性質が、名前による強制を成り立たせています。

重要なのは、プレフィックスは「属性の代わり」ではないということです。プレフィックスは SecureDomain なしといった属性が正しく付いていることを、ブラウザに保証させる仕掛けです。属性を付ける作業がなくなるわけではなく、「正しく付いていなければブラウザが受け取らない」という強制が加わるのです。

プレフィックスの種類

ブラウザが広く強制する標準のプレフィックスは __Secure-__Host- の2種です。まずは2種のうち制約がゆるい __Secure- から見ていきます。

__Secure-

名前が __Secure- で始まる cookie は、次を満たすときだけブラウザが受理します。

  • Secure 属性が付いている
  • 安全な接続(https など)で設定されている

条件を満たさない Set-Cookie は無視されます。最低限「常に Secure で、安全な経路から設定された cookie」であることを名前で保証する、ゆるめのプレフィックスです。

__Host-

名前が __Host- で始まる cookie は、次のすべてを満たすときだけ受理されます。

  • Secure 属性が付いている
  • 安全な接続(https など)で設定されている
  • Domain 属性が無い(= host-only / ホスト限定)
  • Path=/(パスがちょうど /

ポイントは Domain を持てないこと、つまり cookie が host-only(設定したホスト限定)になることです。host-only ゆえに、この cookie は設定したホストそのものにしか送られず、他のサブドメインへは送られません。そして host-only ゆえに、別のサブドメインから同名の __Host- cookie を設定しても、それは設定元ホスト限定の別 cookie にしかならず、本来のホストの cookie を上書き・注入できません。先ほどの「サブドメインからの上書き/注入」の穴を、名前の制約だけで直接塞げるわけです。「上書き禁止フラグ」があるのではなく、「Domain を持てない(host-only)」という制約から論理的にこの性質が導かれている、という因果関係に注意してください。

大文字小文字の注意

仕様(RFC 6265bis)上の正規表記は __Secure- / __Host- です。ただしブラウザはプレフィックスを大文字小文字を区別せずに照合して条件を強制します。つまり __host-__HOST- と綴っても、ブラウザはプレフィックス cookie とみなして同じ受理条件を課します。「小文字にすれば制約を回避できる」ということはありません。

__Http- / __Host-Http-(補足)

HttpOnly を名前で保証しようとする __Http- / __Host-Http- というプレフィックスも目にすることがありますが、これらはブラウザが広く強制する標準2種ではありません__Http- / __Host-Http- は RFC 6265bis には含まれておらず、より新しい IETF の Internet-Draft(layered-cookies)が提案しているものです。RFC 化されておらず、現状の主要ブラウザでは広くは効きません。実用上、頼りにできる強制されるプレフィックスは __Secure-__Host- の2種だと考えてください。

標準2種の対比表

受理条件 __Secure- __Host-
Secure 必須 必要 必要
安全な接続(https など)で設定 必要 必要
Domain 属性 制限なし 不可(host-only)
Path=/ 必須 制限なし 必須
プレフィックスによるサブドメイン分離(上書き/注入耐性) なし あり

具体例:Set-Cookie と Laravel

まず生の Set-Cookie

OK / NG を Set-Cookie ヘッダで対比します。

Set-Cookie: __Host-session=abc123; Secure; Path=/; HttpOnly; SameSite=Lax

OK。https オリジンから設定され、Secure 付き・Domain なし・Path=/ をすべて満たすので保存されます。

Set-Cookie: __Host-session=abc123; Secure; Path=/; Domain=example.com; HttpOnly

NG。__Host-Domain を付けてはいけません。Domain があるためブラウザに無視され、保存されません。

Set-Cookie: __Secure-token=xyz789; Path=/; HttpOnly

NG。__Secure-Secure 属性が必須です。Secure が無いため無視され、保存されません。

HttpOnlySameSite はプレフィックスの受理条件には含まれませんが、実運用では別途付けておくべき属性です(後述)。

続けて Laravel/PHP

Laravel でレスポンスに個別の cookie を付ける場合、グローバルヘルパ cookie() 等で名前にプレフィックスを付け、条件に合う引数を渡します。Laravel の cookie() / response()->cookie() では、名前・値・有効期限(分)に続いて path / domain / secure / httpOnly を指定できます。

// __Host- の条件に合わせる: path='/', domain=null, secure=true
$cookie = cookie(
    '__Host-token',  // 名前にプレフィックス
    $value,
    60,              // 有効期限(分)
    '/',             // $path = '/'
    null,            // $domain = null(host-only)
    true,            // $secure = true
    true             // $httpOnly = true
);

return response('OK')->cookie($cookie);

セッション cookie をプレフィックス化する場合は、config/session.php(および対応する env)の値を __Host- の条件に合わせます。

// config/session.php(関係するキーの抜粋)
'cookie' => env('SESSION_COOKIE', '__Host-laravel-session'), // 名前にプレフィックス
'path'   => env('SESSION_PATH', '/'),        // Path=/
'domain' => env('SESSION_DOMAIN', null),     // Domain なし(host-only)
'secure' => env('SESSION_SECURE_COOKIE', true), // Secure を明示

env で設定するなら次のようになります。

SESSION_COOKIE=__Host-laravel-session
SESSION_SECURE_COOKIE=true
SESSION_PATH=/
# SESSION_DOMAIN は設定しない(null のまま = host-only)

path(既定 '/')と domain(既定 null)は既定のままで __Host- の条件に合致します。一方 secure の既定値は null(リクエストが https のとき自動で secure)なので、条件を確実に満たすには true を明示しておくのが安全です。

なお Laravel は既定で EncryptCookies ミドルウェアにより cookie を暗号化・署名しますが、これは cookie のに対する処理です。名前のプレフィックスに対するブラウザ側の受理判定とは独立しており、プレフィックス名の cookie でも暗号化はそのまま機能します。両者は別の軸の話だと整理してください。

使うときの注意点

ブラウザ対応

__Host- / __Secure- の強制は Chrome 49・Firefox 50(いずれも 2016 年)以降で実装され、現行の主要モダンブラウザで広くサポートされています。ただし非対応のブラウザでは、プレフィックスは「ただの名前」として素通りで受理されます。つまり強制が効かない環境がありうるので、プレフィックスを唯一の防御にせず、保険として Secure などの属性自体は必ず正しく付けておくべきです。プレフィックスは「対応ブラウザでの追加防御」と位置づけてください。

ローカル開発

素の http(非 localhost)では Secure cookie を置けないため、__Secure-/__Host- cookie も設定できません(ブラウザが拒否します)。ここは挙動が安定しています。一方 localhost は多くのブラウザで secure context(安全なコンテキスト)として扱われ、http でも Secure やプレフィックス cookie を設定できることがあります。ただしこの挙動はブラウザやバージョンによって差があり、Safari は localhost の http でも Secure cookie を許可しません。Chrome も挙動が変遷しており、情報源によって記述が食い違います。ローカルでも mkcert などで https を用意して動かすのが無難です。

HttpOnly とは独立

標準2種(__Secure- / __Host-)は HttpOnly を要求も制御もしません。プレフィックスを付けても、JavaScript から cookie が読めるかどうかは変わりません。JS からの読み取りを防ぎたい(XSS 対策)なら、別途 HttpOnly を付ける必要があります。プレフィックスと HttpOnly は別の軸なので混同しないでください。

既存 cookie のリネーム

すでに運用中の cookie の名前を __Host-... に変更すると、ブラウザからは別の cookieとして扱われます。既存のセッションは新しい名前には引き継がれないため、利用者から見るとログアウトしたのと同じ状態になります。移行のタイミングには注意してください。

まとめ

持ち帰りは次の3点です。

  1. cookie の属性は「サーバが正しく付ける」前提で成り立っており、別ホストやサブドメインからの設定、あるいは設定ミスがあると、同じ名前のまま属性だけが緩い cookie を作られて崩れる余地がある。
  2. プレフィックスは名前に受理条件を埋め込み、条件を満たさない cookie をブラウザに拒否させる仕掛け。名前を改ざんすれば別 cookie になるため、名前による強制が効く。
  3. 実用上は __Secure-(要 Secure + 安全な接続)と __Host-(さらに Domain なし + Path=/)の2種。サブドメイン分離まで効く __Host- が最も厳しく、迷ったら __Host- を選ぶとよい。ただし、サブドメイン間で共有したい cookie には __Host- は使えない。

関連記事: (関連)Auth0/Keycloak は…cookie による状態管理(近日公開)

5
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?