跨網域的cookie與資料安全 / Cross domain cookie and data security

在過去美好(?)的年代,cookie的使用限制較少,但隨著網路安全、更嚴謹的CORS,乃至於個人隱私保護,cookie逐漸單純以追蹤瀏覽器行為的工具,而寬鬆的cookie 存取設定也漸漸變成不受到建議的使用方式。

使用 php 的 setcookie() 與 header() 來設定cookie

php裏頭有提供這兩種方式來做設定 cookie,比較方便使用的是 setcookie(),特別是當要設定多個cookie時,如果用 header()函數時,則要記得加上 replace = false (參考 php manual header()),要不然同樣的 header 會 overwrite,變成只有最後一個送出。

// $datetime is the expire datetime
setcookie("userid", 9527, $datetime);
setcookie("token", "8H123UA7SD", $datetime);
setcookie("value1", 9487, $datetime);

// 用 header() 寫變成這樣
header("set-cookie: userid=9527; Expires=" . $datetime . "; ", false);
header("set-cookie: token=8H123UA7SD; Expires=" . $datetime . "; ", false);
header("set-cookie: value1=9487; Expires=" . $datetime . "; ", false);

// node.js 會像這樣, expire 是 _datetime
res.cookie('userid', '9527', {  expires: _datetime });
res.cookie('token', '8H123UA7SD', { expires: _datetime });
res.cookie('value1', '9487', { expires: _datetime });

當使用者第一次瀏覽該頁面時,就會被寫入這些 cookie,而日後再次拜訪此網站的時候,網站可以得知該使用者之前所存下來的cookie內容。

許多網站有提供登入一次後,可以在一段期間內免輸入帳號密碼保持登入狀態,通常都是使用 cookie 來記錄登入 token,這樣就可以免除使用者重複輸入帳號密碼的麻煩。

Cookie domain

Cookie domain 只能是現在所處的網址或是其 parent domain。例如在瀏覽 https://a.domain1.com:8000/test/page 時,domain 預設是 a.domain1.com,其中 port number 不受影響。這時候如果要設定 cookie domain 是 .domain1.com 是沒問題的,而這樣的設定代表該 cookie 可以整個 .domain1.com 通通可以存取。

如果想在這時候設定 b.doamin2.com 是一定會被 browser 給攔下,因為這樣代表可以竄改另一個 domain 的 cookie 資料,有著很大的安全性問題。以前述的登入 token 來說,如果可以被竄改,就代表可以替換成另一個使用者的身分在 b.domain2.com 裏頭使用。

SameSite 與 Secure

在2020年初,主流的幾個 browser 就開始對 cookie 有更嚴格的限制,用 Samesite 屬性來辨識 cookie 的有效作用範圍。共有三類:

Samesite屬性 意義
Strict 必須要是同個網域(first-party)的 request 才能夠傳送 cookie 資料。

這是 https://a.domain1.com/page1

  這裡放了一張圖 https://a.domain1.com/image.jpg    
    這裡放了一個連結到 https://a.domain1.com/page2  
 

當瀏覽 https://a.domain1.com/page1 時,如果被寫入 strict cookie,那麼當點連結 https://a.domain1.com/page2 的時候,則會把 strict cookie 一起跟著送到 https://a.domain1.com/page2 。因為兩者都在 a.domain1.com ,屬於同個網域(first-party)。

但是如果換成是使用者先瀏覽過 https://a.domain1.com/page1 拿到了 strict cookie, 然後再去瀏覽 https://www.domain2.com/page1。

這是 https://www.domain2.com/page1

  這裡放了一張圖 https://a.domain1.com/image.jpg    
    這裡放了一個連結到 https://a.domain1.com/page2  
 

此時使用者去點下 https://a.domain1.com/page2 連結時,因為原本的 cookie 是 strict,所以進到 https://a.domain1.com/page2 的時候,cookie 將不會被跟著帶過去。

Samesite屬性 意義
Lax (預設) Lax 屬性將允許cross-domain request 可以帶著 cookie 一起過去,但僅限於 http get, form get, 和 prerender。過去還允許的 iframe, image, form post, ajax xhttprequest 就通通不行使用了。
所以前述的例子中,從 https://www.domain2.com/page1 點連結到 https://a.domain1.com/page1 的時候,如果 cookie 是被設定為 Lax,則會跟著被傳送過去。

Samesite屬性 意義
None 需要額外設定Secure屬性,且是https才能正常運作。當使用 samesite=none 時,代表任何的 request 都會送 cookie 出去。
但根據Chromium Blog 的說法,這樣的使用方式將會逐漸被封鎖淘汰,主要原因是不小心誤用(或惡意使用)很容易有安全問題,畢竟甚麼 request 都可以讓 cookie 一起跟著過去。

Samesite = None 的安全問題

最大的問題在於如果有個 API 是被用來更改資料,且 cookie 中又放著有權限的 token,那麼向下面這樣的例子就等於惡意網站可以做一些壞事。

這是 https://www.EvilWEB.com/page1

  這裡放了一張圖 https://a.domain1.com/image.jpg    
    這裡是一個畫面上看不到的iframe, 然後用 jquery 拿了 a.domain1.com 的 cookie 呼叫了 API https://a.domain1.com/apiDeleteData  
 

或者是做一個釣魚網頁,當使用者還在狐疑,尚未輸入任何資料的時候,js的程式碼就已經開始在做壞事了。另外還可能出問題的是 Cross-Site Request Forgery (CSRF)

php 裏頭做 samesite 設定

如果用 header() 去寫 cookie 變成這樣,比較不會受到 php 版本影響

header("set-cookie: userid=9527; Expires=" . $datetime . "; Domain=a.domain1.com . "; SameSite=None; Secure", false);
header("set-cookie: token=8H123UA7SD; Expires=" . $datetime . "; Domain=a.domain1.com . "; SameSite=None; Secure", false);
header("set-cookie: value1=9487; Expires=" . $datetime . "; Domain=a.domain1.com . "; SameSite=None; Secure", false);

如果是用setcookie()則要注意一下, php 7.2 (含)以前的版本,則必須寫成

// $datetime is the expire datetime
setcookie("userid", 9527, $datetime, "/;samesite=none", "a.domain1.com");
setcookie("token", "8H123UA7SD", $datetime, "/;samesite=none;secure", "a.domain1.com");
setcookie("value1", 9487, $datetime, "/;samesite=none;secure", "a.domain1.com");

之後的版本寫成

// $datetime is the expire datetime
setcookie("userid", 9527, $datetime, ['samesite' => 'None', 'secure' => true, 'domain' => 'a.domain1.com']);
setcookie("token", "8H123UA7SD", $datetime, ['samesite' => 'None', 'secure' => true, 'domain' => 'a.domain1.com');
setcookie("value1", 9487, $datetime, ['samesite' => 'None', 'secure' => true, 'domain' => 'a.domain1.com');

詳細可以參考 php manual setcookie()

// node.js 會像這樣, expire 是 _datetime
res.cookie('userid', '9527', { domain: 'a.domain1.com', expires: _datetime, sameSite: 'none', secure: true });
res.cookie('token', '8H123UA7SD', { domain: 'a.domain1.com', expires: _datetime, sameSite: 'none', secure: true });
res.cookie('value1', '9487', { domain: 'a.domain1.com', expires: _datetime, sameSite: 'none', secure: true });

OAuth 與跨網域登入

從 login 網站發 oauth request 到 oauth service,並夾帶 identity token 以及 redirect url ,在 oauth service 處可以讀取 oauth service 網站的 cookie, 跟 request uri 參數比對作登入。成功後 redirect 回原本網站,並回傳 oauth 上的使用者資料。

Reference:

  1. SameSite cookies explained
  2. PHP Cookie SameSite 的設定方式
  3. How cookies track you around the web and how to stop them
  4. 網站安全🔒 再探同源政策,談 SameSite 設定對 Cookie 的影響與注意事項
  5. 不想失去追蹤受眾資料 ?! 你該知道的Chrome cookie更新