Dev Study
← Webセキュリティの実務
レッスン 6 / 12·約 9 分·第19期 Webセキュリティの実務

注入されたスクリプトは、そのオリジンそのものとして動く — HttpOnly が守るもの

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回は、別サイトのページが作れない信号で CSRF を防ぐことを見ました。今回は、答えが「同じオリジン」になってしまうコード、ページの内側で動くスクリプトを扱います。

まず結論

ブラウザが確かめられるのは「どこから来たか」までです。ページの中で動くスクリプトは、誰が書いたものであれ、そのオリジンから来たものとして扱われます。HTML の定義は、同じオリジンの行為者を `the same authority` を持つものと前提しています。

HttpOnly が外すのはスクリプト向け API からの読み取りだけで、HTTP 要求には Cookie が付きます。値の持ち出しは防げても、ページの中から認証付きの要求を出して応答を読むことは止まりません。「本人が望んだか」を確かめる手段は、ページの内側に入られた時点で効かなくなります。

? 読む前に予測

セッション Cookie に HttpOnly が付いたページがあります。そのページに最初から置かれた script が document.cookie を読み、続けて同じオリジンの /me を fetch します。何が起きるでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

ブラウザの帳簿に「誰が書いたか」の欄は無い

このコースの芯は、ブラウザが確かめられるのは「どこから来たか」までで、「本人が望んだか」はサーバが自分で確かめるしかない、というものでした。では、要求を出したのがページの内側で動くスクリプトだったら、どうなるでしょうか。

入力がコードに変わる原理は『セキュリティとAIの仕組み』の「なぜ入力がコードになるのか」(SQL の例)で、HTML に値を差し込むと XSS を招く場面は『アプリケーションの土台』の「文字列を返しているだけ」で見ました。今回は入る方法は扱いません。入った後に何ができるかだけを見ます。

出発点は HTML Standard のオリジンの定義です。

Origins are the fundamental currency of the web's security model. Two actors in the web platform that share an origin are assumed to trust each other and to have the same authority. Actors with differing origins are considered potentially hostile versus each other, and are isolated from each other to varying degrees.

同じオリジンの行為者は the same authority(同じ権限)を持つと前提されています。この定義に、そのスクリプトを誰が書いたかという項目はありません。サイトの作者が書いた script も、紛れ込んだ script も、同じオリジンの文書で動けば同じ権限を持ちます。 ここは定義からの教材側の読みです。

では HttpOnly はどこまで効くのでしょうか。6265bis の取り出しアルゴリズム(§5.8.3)の条件はこうです。

If the cookie's http-only-flag is true, then exclude the cookie if the retrieval's type is "non-HTTP".

除外されるのは non-HTTP の取り出し、つまりスクリプト向けの API だけです。HTTP 要求の Cookie ヘッダからは除かれません。 要求をスクリプトが出したかどうかも条件にありません。

ブラウザが見るのは、どのオリジンの文書で動いているかまで
サイトの作者が書いた script
ページに紛れ込んだ script
どちらも同じオリジンの文書で動く
同じオリジンの行為者the same authority と前提される
HttpOnly が外すのは non-HTTP の取り出しだけ
document.cookieHttpOnly の値は出ない
fetch('/me')Cookie が付き、応答も読める
  • ・作者の区別は定義に無い(教材側の読み)
  • ・HttpOnly が止めるのは値を読むことで、値を使った要求ではない
具体実物

最初からページに置いた script で確かめる

注入は再現しません。自前のサーバ(b.test:8443、ローカル証明書)が、最初からページに置いた script を含む HTML を返し、同じ応答で Cookie を2つ設定します。すでにオリジンの内側で動いているコードの代わりです。

GET /app の応答(Set-Cookie は HTTP ヘッダの形に書き直し、乱数は省略。script は表示部分を省略)設定ファイル
Set-Cookie: app_sid=…; HttpOnly; Secure; SameSite=Lax; Path=/
Set-Cookie: ui_theme=dark; Secure; SameSite=Lax; Path=/
 
<script>
(async () => {
const lines = [];
lines.push('document.cookie = ' + JSON.stringify(document.cookie));
const r = await fetch('/me');
lines.push('fetch(/me) -> ' + r.status + ' ' + (await r.text()).trim());
// (結果をページに表示するだけ)
})();</script>

1行目の app_sid が HttpOnly のセッション Cookie、7行目が値を読む操作、8行目が同じオリジンへの要求です。/me は app_sid がセッション表にあれば user=alice を返します。fetch に credentials の指定はありませんが、Fetch Standard §2.2.5 は要求の credentials mode について Unless stated otherwise, it is "same-origin". と定めているので、同じオリジンへは Cookie が付きます。

Playwright 1.63.0 同梱の Chromium 153.0.8010.12(headless)の1回目(サーバログは /me の行から path・cookie・sec-fetch-site 以外のキーを省略)コマンド出力
[chromium 153.0.8010.12] document.cookie = "ui_theme=dark"
[chromium 153.0.8010.12] fetch(/me) -> 200 user=alice
--- server-log.jsonl
{"path":"/me","cookie":"app_sid=154d24f3ae03bd95201732fe629d79db; ui_theme=dark","sec-fetch-site":"same-origin"}

Playwright 1.63.0 同梱の Chromium 153・Firefox 155.0・WebKit 26.6 を headless で各2回動かし、6回とも同じでした。市販のブラウザそのものではありません。スクリプトから見える Cookie は ui_theme=dark だけで、app_sid は出ません。それでも /me の要求には app_sid が付き、200 が返り、スクリプトは応答の user=alice を読めています。同じオリジンなので CORS も関係しません。

値は読めないが、値で認証された要求は出せて、応答も読める。 HttpOnly が効いている範囲と効いていない範囲が、1つのログに並んでいます。

✓ ここまでの確認

6265bis の取り出しアルゴリズム(§5.8.3)で、http-only-flag が真の Cookie が除外されるのはどんな場合ですか。

抽象原理

防げるのは持ち出し、防げないのは内側からの要求

この限界は、6265bis には明文では書かれていません。§8 Security Considerations を HttpOnly で検索しても0件でした。6265bis から言えるのは §5.8.3 の条件までです。

明文で書いているのは OWASP の Session Management Cheat Sheet です。

The HttpOnly cookie only protects the confidentiality of the cookie; the attacker cannot use it offline, outside of the context of an XSS attack.

守るのは機密性で、値を外に持ち出して別の場所で使うことを防ぎます。同じ段落は、HttpOnly を session ID の窃取を防ぐために mandatory とも書いています。「HttpOnly は XSS に無意味」は逆向きの誤りです。 防げるものと防げないものの両方を言えて、初めて正確です。

防げないほうは、Cookie だけの話ではありません。ページの内側のコードは、同じオリジンの応答も、ページ内の hidden フィールドも読めます。攻撃側のページが作れない信号をサーバに渡すという CSRF 対策の前提は、作る側がページの内側にいると崩れます。OWASP の CSRF Cheat Sheet は冒頭で警告しています。

Remember that Cross-Site Scripting (XSS) can defeat all CSRF mitigation techniques!

ただし同じ段落は CSRF tokens are still essential for web applications that rely on cookies for authentication. と続きます。トークンが不要になるのではなく、トークンの前提となる境界を内側から越えられる、ということです。

XSS Prevention Cheat Sheet も、Cookie の属性について don’t prevent the execution of malicious content or address the root cause of the vulnerability と書きます。根本は、入れないこと(出力時のエスケープ)の側にあります。

具体実物

ブラウザの答えは正しい。だから区別がつかない

同じログの /me の行には、もう1つ情報があります。sec-fetch-site: same-origin です。

Sec- で始まるヘッダは、Fetch Standard で forbidden request-header とされています。

These are forbidden so the user agent remains in full control over them.

スクリプトは自分で書き換えられず、値はブラウザが付けたものです。そしてその答えは正しい。要求は確かに同じオリジンの文書から出ています。

このヘッダは「どこから来たか」を偽れない形でサーバに伝えるので、別サイトからの要求を見分けるには役に立ちます。しかし、ページの内側で動くコードの要求は、サイト自身の script が出す要求と同じ same-origin に分類されます。ブラウザの報告が正確であることと、その要求を本人が望んだことは、別の問いです。

✓ ここまでの確認

ページ内から出た /me の要求には `sec-fetch-site: same-origin` が付いていました。このことから言えることはどれですか。

⇄ 2つを見比べる
具体例 A

HttpOnly の Cookie

スクリプト向けの API からは値が消えます。しかし HTTP 要求には付くので、ページ内の fetch は認証付きで通り、応答も読めます。値の持ち出しは防げますが、値を使うことは防げません。

実測(Playwright 同梱版の headless、3エンジン×各2回、6回とも同じ)表
操作 結果
document.cookie ui_theme=dark のみ
fetch('/me') 200 user=alice
/me の要求の Cookie app_sid=…; ui_theme=dark
具体例 B

Sec-Fetch-Site ヘッダ

スクリプトが書き換えられないヘッダで、ブラウザが正確な値を付けます。ページ内の fetch には same-origin と付き、サイト自身の script が出す要求と区別がつきません。

同じ実測の /me の要求表
ヘッダ 値
sec-fetch-site same-origin
referer https://b.test:8443/app

片方は「読ませない」、もう片方は「偽らせない」保護です。どちらもページの内側のコードを止められないのはなぜでしょうか。

それぞれが何を基準に判定しているかを並べてみてください。

この章のまとめ

  • HTML の定義では、同じオリジンの行為者は the same authority を持つと前提される。誰が書いたかの項目は無い。
  • 6265bis §5.8.3 で HttpOnly が除外するのは non-HTTP の取り出しだけ。HTTP 要求には付く。
  • 実測(3エンジン×2回): document.cookie に app_sid は出ないが、fetch('/me') は認証付きで 200、応答も読める。
  • 限界の明文は 6265bis §8 には無く、OWASP が only protects the confidentiality と書く。窃取を防ぐ意味では mandatory。
  • Sec-Fetch-Site: same-origin は正確だが、正確さは「本人が望んだか」の答えではない。XSS は CSRF 対策をすべて破りうるが、トークンは依然として必要。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

OWASP Session Management Cheat Sheet は、HttpOnly が守るものを何と書いていますか。

用語集(3語)
HttpOnly
Cookie の属性。6265bis §4.1.2.6 はスクリプト向けの non-HTTP API から Cookie を除くよう指示するものと書く。HTTP 要求の Cookie ヘッダからは除かない。
non-HTTP API
6265bis §2.3 の定義で、Cookie を設定・取得する HTTP 以外の仕組み。例として、スクリプトに Cookie を見せるブラウザの API が挙がっている。
オリジン(origin)
HTML Standard §7.1.1 がセキュリティモデルの基本単位とするもの。共有する行為者は同じ権限を持つと前提される。