Safari プライベートブラウズで Cookie は消えるか — HttpOnly Cookie と Next.js の正しい理解

きっかけ

QRコードを使った注文フォームシステムを開発していたとき、こんな疑問が出ました。

「iPhoneのSafariでプライベートブラウズしたら、Cookieが消えてQRトラッキングが壊れるのでは?」

調べてみると、「Cookieが消える」という話は本当だが、それはCookieの種類による ということがわかりました。開発の現場でよく混同されるポイントなので、整理します。


Cookieには2種類ある

Cookieは誰が書くかによって性質が変わります。

1. JavaScriptが書くCookie(クライアントサイド)

// ブラウザ上で実行されるJavaScript
document.cookie = "user_id=abc123; path=/"

document.cookie でブラウザから書いたCookieです。JavaScriptから読み書き両方できます

2. サーバーが書くCookie(HttpOnly)

HTTP/1.1 200 OK
Set-Cookie: session_token=xyz; HttpOnly; Secure; SameSite=Lax

サーバーがHTTPレスポンスヘッダーの Set-Cookie で送るCookieです。HttpOnly フラグが付いているとき、JavaScriptからは一切読み書きできません


Safari ITPが制限するのはどちらか

SafariのITP(Intelligent Tracking Prevention)が主に制限するのはクロスサイトのトラッキングです。具体的には次のケースです。

  • document.cookie でJavaScriptが書いたCookie
  • サードパーティ(別ドメイン)から発行されたCookie
  • localStorageIndexedDB への第三者アクセス

同一ドメインのサーバーが Set-Cookie で発行した HttpOnly Cookie はITPの制限対象外 です。


Next.jsでの実装

今回のシステムでは、QRコードのトラッキングCookieをサーバーサイドで発行しています。

① middleware.ts(QRスキャン時)

// /porder/apply?src=<slug> へのアクセスをmiddlewareで捕捉
export default clerkMiddleware(async (auth, req) => {
  const url = new URL(req.url)
  if (url.pathname === "/porder/apply") {
    const src = url.searchParams.get("src")
    if (src) {
      const response = NextResponse.next()
      response.cookies.set("porder_qr_src", src, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production",
        sameSite: "lax",
        maxAge: 60 * 60 * 24 * 30, // 30日
      })
      return response
    }
  }
})

NextResponse.next() に対して cookies.set() しているので、これはサーバーが送る Set-Cookie ヘッダーです。JavaScriptは関与しません。

② Server Action(注文時)

"use server"
import { cookies } from "next/headers"

export async function submitOrder(formData: FormData) {
  const cookieStore = await cookies()
  // サーバーサイドでCookieを読み取る
  const orderSource = cookieStore.get("porder_qr_src")?.value ?? null

  await prisma.personalOrder.create({
    data: {
      // ... 他のフィールド
      orderSource, // QRコードのスラッグを記録
    },
  })
}

next/headerscookies() はサーバー専用APIです。"use server" がついたServer Action内でのみ動作します。


プライベートブラウズでの実際の動作

では、Safariのプライベートブラウズでは何が起きるのか。

操作通常モードプライベートモード
QRスキャン → Cookie受け取り
同じタブで注文完了
タブを閉じてCookieが消える❌(期限まで残る)✅(消える)
別タブで同URLを開き直す△(Cookieなし)

HttpOnly Cookieはプライベートモードでも受け取れます。 問題になるのは「タブを閉じた後」です。プライベートモードではセッション終了時(タブを閉じたとき)にすべてのCookieが削除されます。


実際に問題になるケース

今回のQRトラッキングで問題になりうるのは、次のシナリオだけです。

1. QRをスキャン(Cookieセット)
2. 何らかの理由でタブを閉じる
3. ブックマークやメールのリンクから開き直す
   → Cookieが消えているため orderSource が null になる

このシナリオはプライベートモード限定かつ「タブを閉じた場合」のみ。一連の操作を同じタブで完結するなら問題ありません。 チラシを手にしたお客様がカメラでQRを読んでその場で注文する、という想定フローでは実用上の支障はないと判断しました。


まとめ

クライアントサイドCookieHttpOnly Cookie
発行者JavaScriptサーバー(HTTPヘッダー)
JS から読み取り×
XSS耐性弱い強い
Safari ITP の影響受ける受けない(同一ドメイン)
プライベートモードでの消去タブを閉じたときタブを閉じたとき

セッション管理やトラッキングCookieは、可能であれば HttpOnly + SameSite=Lax でサーバーから発行するのがセキュリティ・ITP耐性ともにベターです。Next.jsの場合、middleware または Server Action の cookies() API を使えばクライアントJavaScriptを一切使わずにCookieを制御できます。


参考

シェアポストLINEシェア