Set-CookieのMax-Age=1wは期限なしになる理由とChrome実測
Set-Cookie: session=abc; Max-Age=1w と書くと、1週間有効なクッキーにはならない。1w は整数ではないので Max-Age 属性ごと無視され、ブラウザを閉じると消えるセッションクッキーになる。エラーも警告も出ないので、「ログインが1週間もたない」という形でしか表に出ない。
この記事では、Max-Age と Expires の解釈を RFC 6265 と、その改訂版の draft-ietf-httpbis-rfc6265bis で確認し、16通りの Set-Cookie を Chromium 152 に実際に保存させて、何秒の期限になったかを測る。仕様どおりにならなかったケースが2件あった。
仕様: Max-Age は「先頭が数字か - 」で「残りが全部数字」
RFC 6265 の 5.2.2 The Max-Age Attribute は、Max-Age の値を次の順で処理するとしている。
- 値の先頭が数字でも
-でもなければ、その属性(cookie-av)を無視する - 2文字目以降に数字以外の文字があれば、その属性を無視する
- 整数に変換し、0以下なら「表現できる最も早い日時」、つまり即時失効にする
改訂版の draft-ietf-httpbis-rfc6265bis-22(2025年12月1日版)の 5.6.2 も同じ手順で、先頭の条件が「数字、または - に続く数字」に少し厳しくなり、空の値は無視、と明記された。加えて、整数にした値を cookie-age-limit と比べて小さい方を使う手順が入った。5.5 はこの上限を「400日(34560000秒)より先にしない(SHOULD NOT)」としている。Expires にも同じ上限がかかる(5.6.1)。
Max-Age と Expires の両方があるときは Max-Age が勝つ。RFC 6265 の 4.1.2.2 にそう書かれており、保存の手順(RFC 6265 の 5.3、6265bis の 5.7)でも、Max-Age 属性があればそれで期限を決め、無いときだけ Expires を見る。同名の属性が複数あるときは、属性リストの最後のものを使う。
まとめると、仕様上は次のようになる。
1w、60.5、"60"(引用符つき)、+60は、どれもMax-Age属性ごと無視される- 無視された結果
Max-AgeもExpiresも無ければ、セッションクッキーになる - 無視された
Max-Ageは属性リストに入らないので、Max-Age=7200; Max-Age=abcは7200になるはず
Chromium 152 で16通りを実測
Node.js v24.11.1 で localhost に小さなサーバーを立て、1回のレスポンスで16個の Set-Cookie を返した。ブラウザは Chromium 152(Chrome/152.0.7977.167、Windows 11)。保存されたクッキーの期限は、ページ側の JavaScript で Cookie Store API の cookieStore.getAll() を呼び、expires から現在時刻を引いて秒にした。expires が null ならセッションクッキー、一覧に無ければ保存されなかったと判定している。
Set-Cookie の属性 |
Chromium 152 の結果 | 仕様(6265bis-22)の期待 |
|---|---|---|
Max-Age=3600 |
3600秒 | 3600秒 |
Max-Age=1w |
セッション | セッション |
Max-Age=+60 |
60秒 | セッション |
Max-Age=60.5 |
セッション | セッション |
Max-Age=0 |
保存されない | 保存されない |
Max-Age=-1 |
保存されない | 保存されない |
Max-Age=60; Expires=<1日前> |
60秒 | 60秒 |
Expires=<1日後>; Max-Age=0 |
保存されない | 保存されない |
Max-Age=99999999 |
34560000秒 | 34560000秒 |
Expires=Fri, 31 Dec 2100 00:00:00 GMT |
34560000秒 | 34560000秒 |
Max-Age=60; Max-Age=7200 |
7200秒 | 7200秒 |
Max-Age=7200; Max-Age=abc |
セッション | 7200秒 |
Max-Age="60" |
セッション | セッション |
Max-Age= 60 |
60秒 | 60秒 |
Expires=2026-12-01 |
セッション | セッション |
Max-Age=00060 |
60秒 | 60秒 |
読み取れることは次のとおり。
- 整数でない
Max-Ageは、エラーにならずセッションクッキーになる。1w、60.5、"60"がそうだった。「1週間」「1分半」のつもりで書いても、ブラウザを閉じると消える。 Max-Ageは書いた順番に関係なくExpiresに勝つ。1日前のExpiresがあってもMax-Age=60なら60秒残り、1日後のExpiresがあってもMax-Age=0なら保存されない。- 400日の上限は実際にかかっている。約3年分の
Max-Age=99999999も、2100年のExpiresも、どちらもちょうど34560000秒(400日)に切り詰められた。 Expires=2026-12-01はセッションになった。RFC 6265 の 5.1.1 の日付解析は、時刻・日・月名・年がそろわないと失敗とする。-は区切り文字として扱われ、12は月名(Decなど)ではなく、時刻も無いので解析に失敗し、Expiresが無視される。ISO 8601 の日付は Cookie の日付としては読めない。
仕様と食い違ったのは太字の2件だった。
Max-Age=+60は、仕様では先頭が数字でも-でもないので無視されるはずだが、Chromium 152 は60秒として受け入れた。Max-Age=7200; Max-Age=abcは、仕様ではabcだけが無視されて7200が残るはずだが、Chromium 152 はセッションクッキーにした。最後のMax-Ageの値だけを見て、それが読めなければMax-Age全体を無かったことにしているように見える。
どちらも、送る側が正しい整数を1回だけ書いていれば起きない。ただ、フレームワークの設定とリバースプロキシの両方が Max-Age を足すような構成では2件目の形になりうるので、「後ろに壊れた値が付くと前の正しい値も消える」ことは覚えておきたい。Firefox と Safari では確かめていない。
実際に試す
Node.js があれば試せる(記事では v24.11.1 で確認)。ブラウザ側は Cookie Store API を使うので、Chromium 系のブラウザで開く。localhost は安全なコンテキスト扱いなので、HTTP のままで cookieStore が使える。
1. ブラウザに16通りの Set-Cookie を保存させる
1 | |
node server.js で起動し、ブラウザで http://localhost:8765/set を開く。/set がクッキーを付けて /read にリダイレクトし、/read が保存結果を表示する。Chromium 152 での表示は次のとおり(Expires の日付は実行時刻で変わる)。
1 | |
サーバーが実際に送ったヘッダーは curl -i http://localhost:8765/set で確認できる。2回目以降に試すときは、前回のクッキーが残っているので、開発者ツールの Application タブで localhost のクッキーを消してから開く。
2. 仕様どおりの Max-Age 判定を書く
サーバー側で Set-Cookie を組み立てるときや、ログに残ったヘッダーを調べるときのために、6265bis-22 の 5.6.2 と 5.7 の手順どおりに Max-Age を判定する関数を書いた。
1 | |
1 | |
属性値の前後の空白は、属性の解析の段階で取り除かれるので、Max-Age= 60 は60秒になる(Chromium でも60秒だった)。この関数と1の実測を見比べると、+60 と 7200; abc の2行だけが食い違う。送る側としては、どちらの解釈でも同じ結果になるよう、Max-Age は符号なしの整数で1回だけ書くのが安全である。
ハシトシステムの Set-Cookie 解析では
ブラウザで使える Set-Cookie 解析(ハシトシステム) は、貼り付けた Set-Cookie の期限が Max-Age と Expires のどちらで決まったかを表示する。2026-10-31 時点のコードでは、Max-Age を /^-?\d+$/ で判定して整数でなければエラーを出すので、Max-Age=1w や 60.5 が無視されることはこのツールで確かめられる。
一方、今回の実測と比べると、次の点はツールの表示とブラウザの実際の挙動が一致しない。
Expiresの解釈に JavaScript のDate.parseを使っているため、Expires=2026-12-01を有効な日付として読む。Chromium 152 は Cookie の日付として読めずセッションクッキーにした。- 400日の上限は反映しておらず、
Max-Age=99999999はそのままの秒数で表示される。 Max-Age=+60は仕様どおり「読めない」と表示するが、Chromium 152 は60秒として受け入れた。
ツールの結果は「仕様上の解釈」として読み、ブラウザでの最終的な結果は上の server.js のような実測で確かめるのが確実である。
SameSite=None と CORS の組み合わせについては CVE-2025-34291 に学ぶ CORS と SameSite=None の記事、HTTP ヘッダーの文字コードの話は Basic 認証の btoa が日本語で落ちる記事 にも書いた。
まとめ
Max-Ageの値は「先頭が数字(6265bis では-と数字も可)、残りが全部数字」でなければ、属性ごと無視される。1w、60.5、"60"はセッションクッキーになる。Max-AgeとExpiresの両方があれば、順番に関係なくMax-Ageが勝つ。- 6265bis では期限の上限が400日(34560000秒)で、Chromium 152 は
Max-AgeにもExpiresにもこの上限をかけた。 Expires=2026-12-01のような ISO 8601 の日付は Cookie の日付として読めず、無視される。- Chromium 152 は
Max-Age=+60を受け入れ、Max-Age=7200; Max-Age=abcをセッションにした。どちらも仕様の手順とは異なるので、Max-Ageは符号なしの整数で1回だけ書く。
参考
- RFC 6265 HTTP State Management Mechanism: 4.1.2.2 The Max-Age Attribute、5.1.1 Dates、5.2.2 The Max-Age Attribute、5.3 Storage Model
- draft-ietf-httpbis-rfc6265bis-22 Cookies: HTTP State Management Mechanism: 5.5(クッキーの期限の上限)、5.6.1 The Expires Attribute、5.6.2 The Max-Age Attribute、5.7 Storage Model