為什麼要關心 SameSite#

Cookie 是網頁服務維持狀態的關鍵機制,常見於登入、購物車、廣告追蹤等情境。隨著 Cookie 被廣泛使用,隱私與安全議題也跟著浮現,SameSite 屬性正是為了在原本「同 Domain 自動帶 Cookie」的行為上加一層守門,讓瀏覽器能依據情境決定是否要附帶。

Chrome 從 84 版開始,把沒有設定 SameSite 的 Cookie 預設為 Lax。若你的服務依賴第三方 Cookie(Third-Party Cookie)卻沒有顯式宣告 SameSite,行為可能會悄悄改變。

每個 Cookie 都會綁定一個 Domain,這個 Domain 來自 Set-Cookie 寫入時所屬的網址。在使用者當下瀏覽的網址視角下:

  • 若 Cookie 的 Domain 與目前頁面相符,就是 First-Party。
  • 若兩者不同,那這個 Cookie 對目前頁面而言就是 Third-Party。

舉例來說,瀏覽 a.com 時頁面對 third-party.com 發出請求,瀏覽器會自動帶上對應的 Cookie。後續換到 b.com 時,只要再請求 third-party.com,那把 Cookie 仍會被帶過去——對 a.comb.com 來說,那都是 Third-Party Cookie。但如果使用者直接造訪 third-party.com,同一個 Cookie 對該頁面就是 First-Party。

同源(Same-Origin)與同站(Same-Site)#

SameSite 比對的是「Site」,不是「Origin」。兩者經常被混用,但定義不同:

Origin#

Origin 由 Scheme、Host、Port 組成。三者完全相同才算 Same-Origin,任何一項不同就是 Cross-Origin。

Site#

Same-Site 牽涉 eTLD(Effective Top-Level Domain)。所有 eTLD 列在 Public Suffix List ,Site 由 eTLD 加上前一段網域組成:

  • github.io 在 Public Suffix List 內,所以 a.github.iob.github.io 屬於不同 Site(Cross-Site)。
  • .com 在清單內、example.com 不在,因此 a.example.comb.example.com 同樣屬於 example.com 這個 Site(Same-Site)。

Site 不包含 Port,Port 不同也算 Same-Site。

沒有 SameSite 會發生什麼事#

CSRF (Cross-Site Request Forgery)#

假設使用者已登入 example.com 並取得 Cookie,當他造訪惡意網站 evil.com,惡意頁面可以對 example.com/pay?amount=1000 發出 POST 請求。瀏覽器照舊把 example.com 的 Cookie 帶上,伺服器無從分辨這次請求究竟是使用者主動發起,還是被偷渡——使用者就在不知情下完成了一筆轉帳。

過去的 Cookie 沒有「只在 First-Party 環境送出」的開關,所以只要 Domain 相符,瀏覽器一律附帶。Server 沒辦法分辨來源、Client 也浪費流量送出沒必要的 Cookie。SameSite 屬性正是補足這塊。

三種 SameSite 設定#

SameSite 有三個值。沒設定時不同瀏覽器會走不同預設行為,目前 Chrome 走 Lax

Strict#

只在 First-Party 情境帶 Cookie。語意最嚴格,但會帶來體驗問題:使用者在 example.com 點擊 fb.com 的貼文連結時,即便他先前登入過 fb.com,導向後也不會帶 Cookie,會被視為未登入。

Strict 適合付款、刪除這類「動作型」的 Cookie。

Lax#

Lax 在以下情境即便是 Cross-Site 仍會送出 Cookie:

  • 直接在網址列輸入網址。
  • 點擊 <a href="..."> 連結。
  • GET 表單送出 <form method="GET">
  • <link rel="prerender" href="..."> 之類的背景預載。

這些情境的共通點:都是 GET,並且會觸發頁面導航(Navigation)。如此一來既能避開 Strict 強制重新登入的不便,也不會在使用者瀏覽其他網站時不小心送出 Cookie。

Lax + POST 的彈性#

為了避免破壞既有登入流程,Chrome 在 SameSite=Lax 上保留了一段過渡期:Cookie 設定後的兩分鐘內,只要是觸發 Top-Level 導航的請求(例如 POST 表單送出),不論 Method 都會帶上 Cookie。詳情見 Chromium 上的討論串

None#

想要在 Cross-Site 情境送出 Cookie 必須宣告 SameSite=None; Secure。換句話說,現在連在開發機測試第三方 Cookie 也得先準備 https://localhost

Set-Cookie: name=value; SameSite=None; Secure

另外用 XHR 或 Fetch 發出 Cross-Origin 請求時,要額外設定 withCredentials: true 才會帶 Cookie,且 Response 的 Set-Cookie 才會生效;伺服器端也必須回 Access-Control-Allow-Credentials: true,前端 JavaScript 才能讀到 Response。

給尚未支援的瀏覽器留後路#

並非每個瀏覽器都採用最新的 SameSite 規則,過渡期可考慮兩種 Workaround。

幾乎能涵蓋所有瀏覽器,缺點是 Cookie 會雙倍:

Set-Cookie: name=value; SameSite=None; Secure
Set-Cookie: name-legacy=value; Secure

伺服器讀取時依序判斷:

if (req.cookies["name"]) {
  // 有新版就用新的
  cookieVal = req.cookies["name"];
} else if (req.cookies["name-legacy"]) {
  // 否則 fallback 舊的
  cookieVal = req.cookies["name-legacy"];
}

伺服器以 Request 的 User Agent 判斷瀏覽器,再決定要送哪一種 Set-Cookie。好處是不必修改解析端,但 UA 字串可變因素多,誤判機率較高。

重點回顧#

  • 沒指定 SameSite 的 Cookie 會被當作 SameSite=Lax,在 Cross-Site 情境下無法送出。
  • 想在 Cross-Site 情境送 Cookie,必須明確宣告 SameSite=None; Secure
  • 可以用 SameSite sandbox 確認當前瀏覽器是否已套用最新規則。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10251288