在過去美好(?)的年代,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 就通通不行使用了。 |
Samesite屬性 | 意義 | None | 需要額外設定Secure屬性,且是https才能正常運作。當使用 samesite=none 時,代表任何的 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 上的使用者資料。