你有沒有想過,為什麼登入網站後不用一直重新輸入帳號密碼?
這背後的功臣就是 Cookie。
但 Cookie 這個方便的機制,也可能被駭客利用來「冒充你」發送請求。
這篇文章會用一個「游泳」的比喻開始,帶你理解 Cookie 的運作原理,再一步步拆解駭客如何發動 CSRF 攻擊,以及我們該如何防禦。
為什麼網站需要 Cookie?從「手牌」說起
想像你去游泳池。
除了身上的泳衣,最重要的東西是什麼?
答案是:鑰匙手環。
因為你會把貴重物品放在置物櫃裡,而只有你的鑰匙手環才能打開那個櫃子。
鑰匙手環就像是「驗證你身份」的東西。
在網路世界裡,我們同樣需要某種東西來驗證使用者身份。
最直覺的方式就是帳號密碼。
但問題來了:如果每次瀏覽網頁都要輸入帳號密碼,會有多煩?
HTTP 是「無狀態」的,這代表什麼?
HTTP 協定在設計時就被設定為「無狀態」(stateless)。
這是什麼意思呢?
假設你用瀏覽器訪問一個需要登入的網站,輸入帳號密碼後,伺服器驗證成功,回傳屬於你的內容。
你可能覺得:「我已經跟伺服器打過交道了,應該很熟了吧?」
很可惜,按照 HTTP 無狀態的設計,你下一次發送請求時,伺服器根本不記得你是誰。
每一次請求,伺服器都會問你:「你是誰?請出示帳號密碼。」
這種設計簡直枯燥無味,對吧?
Cookie 是什麼?讓瀏覽器幫你記住身份
為了解決這個問題,Cookie 就登場了。
運作方式是這樣的:
第一步:你輸入帳號密碼登入網站
這部分很直覺,就是你在登入頁面輸入帳號和密碼,然後按下登入按鈕。
第二步:伺服器驗證成功後,產生 Session ID
伺服器收到你的帳號密碼,比對資料庫確認沒問題後,會產生一串「隨機字串」。
這串隨機字串叫做 Session ID,中文是「會話識別碼」。
你可以把它想像成游泳池櫃台給你的鑰匙手環號碼。
每個人的號碼都不一樣,伺服器靠這個號碼來辨識你是誰。
第三步:伺服器把 Session ID 放在回應的 Set-Cookie 標頭裡
伺服器會把這串 Session ID 放在 HTTP 回應(Response)的 Set-Cookie 標頭裡,傳給瀏覽器。
HTTP 標頭就是用來放置額外資訊的地方。
實際的 HTTP 回應看起來像這樣:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz789
Content-Type: text/html
<html>登入成功!</html>這裡的 Set-Cookie: session_id=abc123xyz789 就是伺服器告訴瀏覽器:「把這串字串存起來」。
第四步:瀏覽器把 Session ID 儲存在 Cookie 中
瀏覽器收到回應後,會自動把 session_id=abc123xyz789 這串資料儲存起來。
儲存的位置就叫做 Cookie。
你可以在瀏覽器的開發者工具中找到專門存放 Cookie 的地方,查看目前儲存了哪些網站的 Cookie。
第五步:之後每次發送請求,瀏覽器自動帶上 Cookie
當你在這個網站瀏覽其他頁面時,瀏覽器會自動把剛才儲存的 Session ID 放在 HTTP 請求(Request)的 Cookie 標頭裡送出。
實際的 HTTP 請求看起來像這樣:
GET /profile HTTP/1.1
Host: example.com
Cookie: session_id=abc123xyz789注意這裡的 Cookie: session_id=abc123xyz789,這就是瀏覽器自動帶上的身份證明。
第六步:伺服器驗證 Session ID,確認你的身份
伺服器收到請求後,會檢查 Cookie 標頭裡的 Session ID。
如果這串 ID 在伺服器的記錄中存在,伺服器就知道「這是剛才登入的那個使用者」,然後回傳屬於你的資料。
整個流程不需要你再次輸入帳號密碼,一切都是自動完成的。
Cookie 方便了使用者,因為不用一直輸入帳號密碼。
也方便了開發者,因為可以輕鬆記錄使用者的狀態和設定。
但是,Cookie 也方便了駭客。
駭客如何利用 Cookie?CSRF 攻擊原理
駭客想利用你的 Cookie 來冒充你,但要怎麼拿到呢?
先來理解一個重要觀念:Cookie 是依照網域分開儲存的。
什麼意思呢?
當你登入 A 網站時,瀏覽器會儲存 A 網站的 Cookie。
當你登入 B 網站時,瀏覽器會儲存 B 網站的 Cookie。
這兩份 Cookie 是完全分開的,就像這樣:
雖然兩邊的 Cookie 名稱都叫 session_id,但它們是各自獨立的。
A 網站只能讀取自己那份 abc123,拿不到 B 網站的 xyz789。
B 網站也只能讀取自己那份 xyz789,拿不到 A 網站的 abc123。
這是瀏覽器的安全機制,防止網站互相偷看對方的資料。
你可以試試看:同時登入 Facebook 和 Google,打開開發者工具看看,兩邊的 Session Cookie 名稱可能一樣,但內容絕對不同。
既然 A 網站拿不到 B 網站的 Cookie,駭客怎麼攻擊呢?
答案是:駭客根本不需要「拿到」你的 Cookie。
關鍵在於:瀏覽器會自動把 Cookie 發送到對應的網域。
這是什麼意思呢?
假設你已經登入了 B 網站,瀏覽器裡存著 B 網站的 Cookie。
這時候,不管是「誰」發起對 B 網站的請求,瀏覽器都會自動把 B 網站的 Cookie 帶上。
你可能會想:「電腦前就只有我一個人,除了我還會有誰?」
這裡說的「誰」,指的是「哪段程式碼」發起請求。
可能是你自己點擊 B 網站的按鈕,觸發了請求。
也可能是你正在瀏覽的網頁裡,藏了一段 JavaScript 程式碼,這段程式碼偷偷對 B 網站發送請求。
對瀏覽器來說,這兩種情況沒有差別。
瀏覽器不會分辨「這個請求是不是使用者自願發出的」。
它只知道:「喔,要發請求到 B 網站,那我把 B 網站的 Cookie 帶上。」
駭客不需要「拿到」你的 Cookie,只需要「讓你的瀏覽器」替他發送請求就好。
駭客利用的就是這個自動發送的特性。
讓我用一個實際的情境來說明。
情境模擬:駭客如何發動 CSRF 攻擊
第一步:使用者登入網站
假設你登入了一個網站,瀏覽器儲存了代表你身份的 Cookie。
第二步:駭客找到可以攻擊的功能
駭客也會登入這個網站,然後找到「可以修改使用者資料的功能」。
什麼是「可以修改使用者資料的功能」?
就是那些一旦執行,你的帳號狀態就會被改變的操作。
例如:修改個人資料、轉帳、發送訊息、變更密碼等。
假設這個網站有個「修改性別」的功能。
駭客打開瀏覽器的開發者工具,找到這個功能的表單程式碼。
表單程式碼可能長這樣:
<form action="/update-profile" method="POST">
<label>性別:</label>
<select name="gender">
<option value="male">男</option>
<option value="female">女</option>
</select>
<button type="submit">儲存</button>
</form>這段程式碼的意思是:當使用者點擊「儲存」按鈕時,瀏覽器會發送一個 POST 請求到 /update-profile,並帶上使用者選擇的性別資料。
第三步:駭客複製並修改程式碼
駭客把這段表單程式碼複製下來,在自己的電腦建立一個 HTML 檔案。
接著確認三個重要元素:
駭客會:
- 把
action補上完整的網址 - 把
value改成自己想要的值 - 加上一段自動提交表單的 JavaScript
修改後的惡意程式碼長這樣:
<!-- 駭客修改後的表單 -->
<form action="https://example.com/update-profile" method="POST" id="evil-form">
<!-- 把欄位設成 hidden,使用者看不到這個表單 -->
<!-- value 被改成駭客想要的值 -->
<input type="hidden" name="gender" value="female">
</form>
<script>
// 網頁一載入,就自動提交表單,不需要使用者點擊
document.getElementById('evil-form').submit();
</script>注意看這段程式碼的變化:
action從/update-profile變成完整網址https://example.com/update-profile- 表單欄位改成
hidden(隱藏),使用者看不到 value被駭客改成他想要的值- 最後加上一段 JavaScript,讓表單自動提交,不需要使用者點擊任何按鈕
第四步:誘騙使用者點擊
駭客把這份惡意的 HTML 檔案放在一個看起來正常的網頁裡。
可能是一個「美女圖片」網站,或是任何吸引你點擊的連結。
當你用同一個瀏覽器打開這個網頁時,惡意程式碼就會自動執行。
第五步:攻擊成功
因為你之前登入過那個網站,Cookie 還在瀏覽器裡。
當惡意程式碼發送請求時,瀏覽器會自動帶上你的 Cookie。
伺服器收到請求後,驗證 Cookie 沒問題,就會執行操作。
你的資料就這樣被改掉了。
為什麼叫做 CSRF?
讓我們來拆解這個名詞,理解它的意思。
首先是 Request Forgery(請求偽造)
Request 是「請求」的意思,指的是瀏覽器發送給伺服器的 HTTP 請求。
Forgery 是「偽造」的意思。
合在一起,Request Forgery 就是「偽造請求」。
為什麼說是「偽造」呢?
因為這個請求不是你自願發送的,而是駭客透過惡意程式碼,讓你的瀏覽器「代替你」發送的。
對伺服器來說,這個請求看起來完全正常,有正確的 Cookie、正確的格式。
但實際上,你根本不知道自己發送了這個請求。
接著是 Cross-Site(跨站)
Cross 是「跨越」的意思。
Site 是「網站」的意思。
合在一起,Cross-Site 就是「跨網站」。
為什麼說是「跨站」呢?
先來看看正常情況下,網站是怎麼運作的。
當你在 B 網站上點擊「修改性別」按鈕時,表單的 action 通常是相對路徑:
<form action="/update-profile" method="POST">這表示請求會發送到「目前網站」的 /update-profile 路徑。
你在 B 網站,請求就發到 B 網站,這是正常的「同站請求」。
但駭客的惡意網頁不一樣。
駭客的網頁是放在 A 網站上,如果 action 寫 /update-profile,請求就會發到 A 網站,那就沒有攻擊效果了。
所以駭客必須把 action 改成完整網址:
<form action="https://example.com/update-profile" method="POST">這樣一來,你人在 A 網站,但請求卻被發送到 B 網站(example.com)。
這個「從 A 站發送請求到 B 站」的過程,就是「跨站」。
組合起來:CSRF
把這兩個概念組合起來:
- Cross-Site(跨站)+ Request Forgery(請求偽造)
- = CSRF(跨站請求偽造)
完整的意思就是:駭客透過 A 網站,偽造你對 B 網站的請求。
使用者如何防範 CSRF 攻擊?
作為一般使用者,你可以採取以下措施:
方法一:不要隨便點擊陌生連結
如果有人說「這裡有帥哥美女網站」,請忍住。
不要隨便訪問別人給的網址或檔案。
方法二:使用獨立的瀏覽器
如果真的忍不住想點,就用一個「沒有登入過任何網站」的瀏覽器來打開。
這樣就算有惡意程式碼,也沒有 Cookie 可以利用。
方法三:登入後記得登出
每次使用完網站,記得點擊登出。
這樣代表會話的 Cookie 就會被清除,駭客也就沒有可利用的東西了。
但說實話,使用者很難做到這些
上面介紹的三種方法,說起來容易,做起來很難。
不隨便點連結?但有時候連結看起來就是很正常。
用獨立的瀏覽器?誰會為了安全特地開另一個瀏覽器?
每次都登出?大多數人根本懶得登出,下次還要重新輸入密碼很麻煩。
所以實際上,防範 CSRF 攻擊的責任主要落在開發者身上。
使用者能做的有限,但開發者可以從根本上阻止這類攻擊發生。
開發者如何防範 CSRF 攻擊?
如果你是開發者,防範 CSRF 攻擊是你的責任。
回顧一下 CSRF 攻擊成功的關鍵:瀏覽器會自動把 Cookie 發送到對應的網域。
所以防護的核心思路是:讓伺服器能夠分辨「這個請求是不是使用者自願發出的」。
主要有兩種常見的防護方式:
- CSRF Token:在表單中加入一個隨機的驗證碼,駭客拿不到這個驗證碼就無法偽造請求
- SameSite Cookie:直接告訴瀏覽器,跨站請求時不要帶上 Cookie
接下來分別介紹這兩種方式。
防護方式一:CSRF Token
這是最經典的防護方式。
做法是在表單裡加上一個隱藏的 input 欄位,裡面放一個隨機產生的 Token 值。
每個使用者的 Token 值都不一樣。
還記得前面「修改性別」的表單嗎?加上 CSRF Token 後會變成這樣:
<form action="/update-profile" method="POST">
<!-- CSRF Token:由後端產生,每個使用者都有不同的隨機值 -->
<input type="hidden" name="csrf_token" value="a1b2c3d4e5f6g7h8">
<label>性別:</label>
<select name="gender">
<option value="male">男</option>
<option value="female">女</option>
</select>
<button type="submit">儲存</button>
</form>這裡要特別說明:Token 不是寫死在 HTML 裡的,而是由後端動態產生的。
運作流程是這樣的:
- 你向伺服器請求這個頁面
- 伺服器收到請求後,根據你的身份產生一組專屬的 Token
- 伺服器把這個 Token 塞進 HTML 的
<input>欄位裡,然後回傳給你 - 你的瀏覽器收到的 HTML 就已經包含你專屬的 Token 了
所以每個人打開同一個頁面,看到的 Token 值都不一樣。
駭客無法事先知道你的 Token 是什麼,因為 Token 是在你請求頁面的那一刻才產生的。
這個 csrf_token 的值是伺服器隨機產生的,而且每個使用者都不一樣。
你的 Token 可能是 a1b2c3d4e5f6g7h8,別人的可能是 x9y8z7w6v5u4t3s2。
你可能會問:「駭客不是一樣可以打開瀏覽器,複製這段 Token 嗎?」
答案是:駭客只能拿到自己的 Token,拿不到你的 Token。
為什麼呢?
因為 Token 是伺服器根據「目前登入的使用者」產生的。
當駭客登入網站時,伺服器會給駭客一組 Token(例如 hacker123)。
當你登入網站時,伺服器會給你另一組 Token(例如 a1b2c3d4e5f6g7h8)。
駭客可以看到自己的 Token,但他沒辦法看到你的 Token。
所以就算駭客在惡意網頁裡放上他自己的 Token:
<form action="https://example.com/update-profile" method="POST">
<input type="hidden" name="csrf_token" value="hacker123">
<input type="hidden" name="gender" value="female">
</form>當這個請求發送到伺服器時,伺服器會檢查:「這個 Token 是不是屬於目前登入的使用者?」
結果發現 hacker123 不是你的 Token,驗證失敗,請求被拒絕。
當使用者提交表單時,這個 Token 會被放在 HTTP 的請求主體(body)裡,而不是 HTTP 請求標頭(header)中的 Cookie 欄位。
實際的 HTTP 請求看起來像這樣:
POST /update-profile HTTP/1.1
Host: example.com
Cookie: session_id=abc123xyz789
Content-Type: application/x-www-form-urlencoded
csrf_token=a1b2c3d4e5f6g7h8&gender=male注意看這個請求的結構:
Cookie: session_id=abc123xyz789在標頭裡,這個會被瀏覽器自動發送csrf_token=a1b2c3d4e5f6g7h8&gender=male在請求主體裡,這是表單資料格式(用&符號連接每個欄位),這個不會被自動發送
差別在哪?
Cookie 會被瀏覽器「自動」帶上,駭客可以利用這個特性。
但請求主體裡的資料,必須是「表單裡有的欄位」才會被送出。
駭客的惡意表單裡沒有你的 Token,所以就算請求被發送出去,Token 欄位也會是空的或是錯的。
所以它不會像 Cookie 一樣被自動發送。
駭客因為拿不到這個 Token,自然就無法偽造請求了。
不過 CSRF Token 在實作上會有很多細節要處理,不是那麼容易做好。
防護方式二:SameSite Cookie 屬性
這是一個更簡單的方式,直接從瀏覽器層面解決問題。
只要在設定 Cookie 時,加上 SameSite 屬性就可以了。
還記得前面說的嗎?當你登入網站時,伺服器會在 HTTP 回應的 Set-Cookie 標頭裡設定 Cookie。
加上 SameSite 屬性後,HTTP 回應會變成這樣:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz789; SameSite=Strict
<html>登入成功!</html>注意 Set-Cookie 後面多了 ; SameSite=Strict。
這就是告訴瀏覽器:「這個 Cookie 在跨站請求時不要發送。」
什麼是跨站請求?
還記得前面 CSRF 攻擊的情境嗎?
你在 A 網站(駭客的惡意網站),然後 A 網站的程式碼發送請求到 B 網站(你已登入的網站)。
這種「從 A 網站發送請求到 B 網站」的行為,就是跨站請求。
如果 Cookie 設定了 SameSite=Strict,瀏覽器在跨站請求時就不會帶上 B 網站的 Cookie。
沒有 Cookie,伺服器就不知道你是誰,自然會拒絕這個請求。
SameSite 有三種模式:
Strict 模式(最嚴格)
不允許任何跨站請求攜帶這個 Cookie。
如果你在 A 網站點擊連結到 B 網站,B 網站的 Cookie 不會被送出。
這樣就能完全阻擋 CSRF 攻擊。
但缺點是使用者體驗會受影響。
例如你從 Google 搜尋結果點進某個網站,可能會發現自己沒有登入狀態。
Lax 模式(較寬鬆)
允許部分跨站請求攜帶 Cookie,但有條件限制。
主要是允許「GET 請求」攜帶 Cookie。
因為一般來說,GET 請求不會進行資料的修改。
但如果網站的修改功能使用了 GET 方法,駭客還是有機會攻擊成功。
None 模式(不限制)
不使用 SameSite 保護,允許跨站請求攜帶 Cookie。
但如果你要設定 SameSite=None,瀏覽器會強制要求你同時加上 Secure 屬性。
為什麼呢?
因為 SameSite=None 等於放棄了跨站保護,Cookie 可以在任何情況下被發送。
這樣一來,安全性會大幅降低。
為了補償這個風險,瀏覽器要求 Cookie 必須在 HTTPS 加密連線 下才能傳輸。
HTTP 回應會長這樣:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz789; SameSite=None; Secure
<html>登入成功!</html>Secure 屬性的意思是:這個 Cookie 只能在 HTTPS 連線下傳輸。
如果網站使用的是 HTTP(沒有加密),這個 Cookie 就不會被發送。
簡單來說,SameSite=None; Secure 的組合是在說:「我允許跨站請求帶上 Cookie,但至少要確保傳輸過程是加密的。」
這種設定通常用在需要跨站功能的情境,例如第三方登入、嵌入式內容等。
SameSite 實作範例
假設你是後端開發者,你只需要在設定 Cookie 時加上 SameSite 屬性。
以 Node.js 的 Express 框架為例,程式碼可能長這樣:
// 設定 Cookie,加上 SameSite=Strict
res.cookie('session_id', 'abc123xyz789', {
httpOnly: true,
sameSite: 'strict' // 加上這行就搞定了
});伺服器回應時,HTTP 標頭會變成:
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123xyz789; SameSite=Strict; HttpOnly
<html>登入成功!</html>設定成功後,讓我們看看會發生什麼事。
情境一:正常使用(同站請求)
你在 example.com 網站上點擊「修改性別」按鈕。
因為你在 example.com,請求也發到 example.com,這是「同站請求」。
瀏覽器會正常帶上 Cookie,請求成功。
情境二:CSRF 攻擊(跨站請求)
你訪問了駭客的 evil.com 網站,網頁裡藏了惡意程式碼,偷偷發送請求到 example.com。
因為你在 evil.com,但請求發到 example.com,這是「跨站請求」。
瀏覽器發現 Cookie 設定了 SameSite=Strict,於是拒絕帶上 Cookie。
伺服器收到請求後,發現沒有 Cookie,不知道這是誰發的請求,直接拒絕。
駭客的攻擊失敗。
其他防護方式
除了上面介紹的兩種方式,還有其他防護手段:
- 檢查 Origin 標頭:驗證請求是否來自合法的來源
- 檢查 Referer 標頭:驗證請求的來源網址
- 使用 CORS 設定:限制哪些網域可以發送請求
這些方式可以搭配使用,提供多層防護。
小結
讓我們回顧一下這篇文章的重點:
- Cookie 的作用:讓使用者不用每次都輸入帳號密碼,瀏覽器會自動送出 Cookie 來驗證身份
- CSRF 攻擊原理:駭客利用瀏覽器自動送出 Cookie 的特性,誘騙使用者在不知情的情況下發送偽造請求
- 使用者防護方式:不隨便點擊連結、使用獨立瀏覽器、記得登出
- 開發者防護方式:使用 CSRF Token 或 SameSite Cookie 屬性
- SameSite 三種模式:Strict(最嚴格)、Lax(較寬鬆)、None(不限制)
下次登入網站時,你可以打開瀏覽器的開發者工具,看看 Cookie 裡有沒有 SameSite 屬性。
這會讓你對網站的安全性有更直觀的認識。