跨網域的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更新