CSRF:瀏覽器自動附帶憑證,如何讓別的網站替你發出請求
文字解析 · 1 支影片 · 產生於 2026-10-01 00:54
章節(13)
1. Outline
- 起點 · Cross-Site Request Forgery (CSRF) Explained
從「一個能刪帳號的請求安全嗎」出發,先鋪 cookie 自動附帶與同源政策兩塊地基,演一次攻擊現場,再一路推進到 JSON、Content-Type、CORS 與 Flash + 307 的進階繞法。
2. YouTuber 的思維推導
Cross-Site Request Forgery (CSRF) Explained
PwnFunction · 14m11s · 字幕 en · vision=on (input 指定 vision=true;PwnFunction 是全動畫講解,同源政策三條件、JSON 拼字串的逐步變形、Flash + 307 轉址流程都靠畫面推進,讀圖才接得上作者的推理。) · YouTube
預估 vs 實際耗時
| 階段 | 預估 | 實際 |
|---|---|---|
| fetch | 6s | 1s |
| segment | 58s | 5m00s |
| shot | 30s | 3s |
| analyze | 6m15s | 13m35s |
| render | 5s | – |
作者的推導起點是一個刻意平凡的畫面:一個能刪掉帳號的 web 請求。他不先講漏洞名稱,而是問「這樣安全嗎」,再自己回答「不太安全」,理由是別的網站可以送出一模一樣的跨網域請求,伺服器分不出來源。這個「伺服器怎麼知道請求從哪來」的疑問,就是全片要償還的債。
為了讓這個疑問變得可解,他先鋪兩塊地基:第一,登入後的 cookie 會在每一次對該網域的請求自動附帶——他要觀眾「仔細聽」的就是「自動」兩個字;第二,同源政策擋的是「讀」不是「送」,而且要同網域、同協定、同連接埠三條全中才算同源。這兩塊地基一放好,攻擊就只是把它們相加:attacker.com 送得出請求(同源政策不擋送),而 cookie 自動跟著送(不管你人在哪個網域),於是刪除動作在受害者的 session 裡完成。作者刻意在講完攻擊後才回頭說明「我們一條同源政策的規則都沒有打破」,把讀不到回應和攻擊成功這兩件事分開——這是全片最重要的一次觀念校正。
接著他把敘事從「原理」轉向「工程現實」,用一連串「可是如果…呢」推進:可是實作長什麼樣?(一個 action 指向目標端點、method=POST、再用 JS 自動送出的 form)可是現在 API 都收 JSON,而 form 送的是 key=value 呢?(把大括號和 JSON 內容塞進欄位的 name,尾巴用 Ruby 註解或一組 junk key/value 吃掉,拼出合法 JSON)可是有更省事的路嗎?(直接 fetch,同源政策的錯誤只影響讀回應)可是伺服器要求 Content-Type: application/json 呢?(form 加不了標頭,JS 加標頭要伺服器透過 CORS 明確放行)可是伺服器不放行呢?(用不受同源政策直接管的 Flash 強設標頭,再用會原封保留標頭與 body 的 307 轉址補上 cookie)。每一步都是「上一招的限制」逼出「下一招」,攻擊者不斷把限制轉成條件。
最後他收在兩個現實提醒:CSRF 可以跟其他漏洞串接放大(他自己靠它拿到過遠端程式碼執行),以及有防護不等於安全——token 可能從別的端點外洩,或本身可預測。整條線是:一個請求的來源無法被信任 → cookie 自動附帶 + 同源政策只擋讀 → 攻擊成立 → token 綁定來源意圖 → 攻擊者繞過各種格式與標頭限制 → 防護本身也可能被繞。
推理鏈:每段留給下一段的線索
每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。
- 一個看起來沒問題的刪除請求 → 留下一個尚未回答的問題:伺服器既然分不出請求來自哪個網站,它到底憑什麼認定這個刪除請求是「你」發的?答案的第一塊拼圖,是登入之後那份會自己跟著跑的憑證。
- Cookie 會自動跟著送 → 現在「送出去的請求會自動帶著你的身分」已經成立。剩下的問題是:既然如此,惡意網站為什麼不能乾脆把你的資料整份讀走?擋住這件事的那條規則,正好也決定了 CSRF 的邊界。
- 同源政策:能送不能讀 → 兩塊地基都放好了:cookie 會自動送、同源政策只擋讀不擋送。把它們相加會得到什麼結果,作者要用一個具體的受害場景直接演給你看。
- CSRF 現場:光是點進去帳號就沒了 → 現在有了「什麼都沒按帳號卻沒了」這個結果,卻還沒有過程。下一步必須把這條路徑拆開,看看攻擊者送出的那個請求,跟你自己按下按鈕送出的那個請求,到底差在哪裡。
- 拆解:兩個請求長得一模一樣 → 攻擊怎麼成立已經清楚了,但兩個問題還懸著:那要怎麼防?以及——既然同源政策存在,這個攻擊到底算不算繞過了它?下一段作者兩個一起回答。
- 防禦 anti-CSRF token 與「沒破壞任何規則」 → 原理與防禦都講完了,但那個攻擊頁面實際上長什麼樣還是個謎——什麼都沒按就送出請求,程式碼要怎麼寫?接下來要打開 attacker.com 的原始碼。
- 攻擊頁的程式碼長什麼樣 → 程式碼短得令人不安,但它成立的前提是目標端點收得下 form 送出的 key=value 格式。如果目標要的是 JSON 呢——那條路馬上就走不通了。
- 難題:form 送不出 JSON → 題目已經出好:body 的形狀被瀏覽器寫死成「可控=可控」,而目標要的是一段合法 JSON。下一段作者會一步一步把這個等號吃掉。
- 用表單拼出一段合法 JSON → 拼字串的路走通了,但過程繁瑣又處處是地雷。作者接著要丟出一個讓人洩氣的對比:其實有一條短得多的捷徑。
- 更簡單的做法:fetch → 兩條路都通了,但它們共用同一個致命前提:伺服器不在乎 Content-Type。下一段要拆掉這個前提,看看當伺服器認真檢查標頭時,攻擊者還剩下什麼。
- Content-Type 這道關卡與 CORS → 門被鎖上了,而且鑰匙在防守方手裡。作者最後要示範一種完全不敲這道門的路:如果有一個不受同源政策直接管轄的東西,能自己決定標頭呢?
- Flash 加 307 轉址繞過標頭限制 → 技術面的攻防到此為止,攻擊者已經把能想的路都走過一遍。剩下的問題是:這些技巧在真實世界裡值多少?而防護做好了就真的安全嗎?
- 串接其他漏洞與現況
3. 逐段說明
Cross-Site Request Forgery (CSRF) Explained
1. 一個看起來沒問題的刪除請求 0:00–0:36
作者先丟出一個問題:一個能刪掉使用者帳號的 web 請求,這樣實作安全嗎?他自己回答「不太安全」,因為別的網站可以送出一模一樣的跨網域請求,伺服器根本分不出來源。為了把這件事講清楚,他決定先補基礎再進正題。
推理因為觀眾對「一個能刪帳號的請求」不會有戒心,作者就從這裡下手:他先問「這樣的實作安全嗎」,自答「不太安全」,再給出理由——如果另一個網站送出一模一樣的跨網域請求,伺服器要怎麼知道它從哪來?他沒有馬上回答這個問題,而是宣布要先補基礎。這個順序本身就是推理:問題必須先變得「不可迴避」,聽眾才會願意繞路去看兩塊地基。
AI 補充作者在這 36 秒裡其實藏了一個沒說破的前提:HTTP 請求本身不帶「我是誰、我從哪個頁面發出的」這種可信欄位。伺服器看到的只有方法、路徑、標頭和 body,而這些全部都可以被另一個網站原樣複製出來。Referer 與 Origin 標頭雖然會透露來源,但前者常被隱私設定或代理拿掉、後者當年並非所有情境都會帶,所以「不能只靠請求本身判斷來源」在當時是合理的出發點。也要先分清楚:這裡談的是「誰送出這個請求」(來源),不是「這個請求屬於哪個帳號」(身分)——身分那一塊瀏覽器等一下會自動幫忙補上,正是這個「自動」讓兩者被混為一談。
術語:endpoint
2. Cookie 會自動跟著送 0:36–1:06
第一個基礎:登入之後瀏覽器會存下 cookie,而只要你對那個網站發出請求,cookie 就會自動附帶出去,伺服器靠它確認你已登入。作者特別要聽眾把這句話記牢,因為整個攻擊都建立在「自動」兩個字上。

推理因為上一段已經把「來源不可信」的疑問架好,作者這裡不急著講攻擊,先把伺服器認人的機制講白:登入 → 拿到 cookie → 存在瀏覽器 → 之後每一次請求都自動附帶。他甚至喊出「接下來這句話請仔細聽」,等於在推理鏈上打了一個記號——他知道整個 CSRF 的成立條件就藏在「自動」兩個字裡,而觀眾此刻還沒察覺這是個弱點,只覺得這是理所當然的便利。
AI 補充作者把「自動」講成一句話帶過,但值得補上它是誰在自動:不是你的 JavaScript、也不是網頁作者,而是**瀏覽器**。瀏覽器對 cookie 的判斷依據只有目的地網域(以及 path、Secure 等屬性),完全不看「這個請求是哪個頁面發起的」。所以在瀏覽器眼中,你自己按下按鈕送出的請求,和某個惡意頁面替你送出的請求,都只是「送往 vulnerable.com 的請求」,兩者一樣有資格附上 cookie。 畫面的示意圖也把這件事畫得很誠實:一個瀏覽器視窗、一台伺服器,中間一條標著「+Cookie」的單向箭頭。圖上沒有畫出「使用者點了什麼」「頁面是哪一頁」——因為那些資訊根本不在這條流程裡。上一段的「伺服器分不出來源」與這一段的「瀏覽器不管來源照樣附 cookie」,其實是同一件事的兩端:一端不問,另一端不說。
術語:session
3. 同源政策:能送不能讀 1:06–2:19
第二個基礎是同源政策。iframe 可以把別的網站嵌進來,但跨 iframe 讀資料被同源政策擋住,必須同網域、同協定、同連接埠三個條件全中才讀得到。作者強調這條規則之所以重要,正是因為 cookie 會自動送出;規則同樣適用於 Ajax。

推理因為上一段已經讓觀眾意識到 cookie 是自動的,作者接著必須解釋為什麼世界還沒毀滅,否則推理鏈會斷。他的順序是:先用 iframe 舉例(一個網站可以把另一個網站嵌進來),再說跨 iframe 讀不到資料,因為有同源政策;然後點名它為什麼重要——「正是因為 cookie 會自動送出」。這句話把第二段直接扣回來。最後他給出判斷同源的三個條件,並補一句「這不只適用於 frame,也適用於 Ajax」,把規則從特例升級成通則。
AI 補充作者說「跨 iframe 的溝通做不到」,這句話要收窄成「跨來源『讀取對方的內容與回應』做不到」才精確;有明確設計過的溝通管道(例如 postMessage)其實是可行的,同源政策擋的是未經允許的資料存取,不是所有互動。 更關鍵的是他沒有明說、但整支影片都建立在上面的那條界線:同源政策管的是**讀回應**,不是**發請求**。請求照樣送得出去、伺服器照樣會執行、cookie 照樣會附帶,只是瀏覽器不把回應交給發起的頁面。對一個「刪除帳號」這種做完就算數的動作來說,讀不讀得到回應根本無關緊要——傷害在請求抵達的那一刻就已經造成了。 畫面上的比對也值得看一眼:同源不是比對「網址看起來像不像」,而是逐段比對協定、網域、連接埠三個欄位,全等才算同源。圖上特別註明「http 的預設連接埠是 80」,提醒你沒寫出來的部分也在比對之列——所以 http://a.com 與 https://a.com 不同源(協定不同),a.com 與 sub.a.com 也不同源(網域不同)。
「the cross iframe communication is not possible due to something called us same origin policy」
4. CSRF 現場:光是點進去帳號就沒了 2:19–3:06
作者先拆字面:CSRF 是跨站請求偽造,一個網域對另一個網域偽造請求去改動某個值。接著直接演一次:你在 vulnerable.com 有帳號,跑去點了 attacker.com,什麼都沒按,vulnerable.com 上的帳號就被刪掉了。
推理因為基礎已經備齊,作者採用「先給名字、再給災難」的節奏:他先逐字拆 CSRF——cross-site request forgery,一個網域對另一個網域「偽造」請求以改動某個值——把攻擊的定義壓縮成一句話;接著立刻切換成第二人稱敘事,讓你在 vulnerable.com 上有帳號、讓你點進 attacker.com、讓你什麼都沒按,然後宣布帳號沒了。這裡他刻意不解釋原理,因為震撼感本身就是推理的燃料:你已經知道兩塊地基,看到結果卻還沒把它們接起來,那個空缺會逼你想下去。
AI 補充「偽造」這個詞值得停一下。攻擊者偽造的不是內容——請求裡的每個位元組都可以是完全合法、伺服器天天在處理的那種;被偽造的是**意圖**:伺服器以為這是你想做的事。這正好扣回第一段那個沒被回答的問題(伺服器分不出來源),也說明為什麼防禦不可能靠「檢查請求長得對不對」。 另外要留意作者的定義裡「in order to modify some value」(為了改動某個值)不是隨口說的限定。CSRF 的目標一定是會改變狀態的動作,因為第三段已經確定攻擊者讀不到回應——偽造一個只讀資料的請求毫無收穫。這條界線之後會一直有效:所有後續的技巧(表單、fetch、Flash)都是在想辦法把一個**寫入**動作送達,沒有一個在意回應。 還有一個容易被忽略的前提:受害者必須在目標網站處於已登入狀態,攻擊才會生效。攻擊者不需要知道你的帳號密碼,甚至不知道你是誰,他只是借用你的瀏覽器;但如果你根本沒登入,那個請求送到了也不會做任何事。
術語:state-changing request
5. 拆解:兩個請求長得一模一樣 3:06–4:08
作者把剛剛的災難拆成兩條路徑對照:自己按 delete 會送出到 vulnerable.com/delete-my-account,而拜訪 attacker.com 時也有一個請求打到同一個端點,只是來源不同。因為 cookie 不管你人在哪個網域都會自動附帶,這個刪除動作就在你的 session 裡完成了。

推理因為上一段只給了震撼沒給機制,作者這裡改用對照法:左邊是你在 vulnerable.com 按下 delete,送出 vulnerable.com/delete-my-account;右邊是你在 attacker.com 什麼都沒按,卻也有一個請求打到「完全一樣的端點」。兩條路徑唯一的差別只有發起的網域。然後他喊出那句回扣——「現在回想我剛剛說過的:每一個請求,cookie 都會自動送到那個網站,不管你現在人在哪個網域」。到這裡第二段的地基被正式領用:不是攻擊者偷到了你的 cookie,而是瀏覽器主動幫他附上。所以刪除是在「你的 session 裡」發生的,伺服器沒有被騙、它只是照規矩辦事。
- CSameSite cookie:跨站請求時是否附帶 cookie 的旗標,2019 年起預設開啟→ 畫地圖
- EDevTools 顯示攻擊請求標頭裡其實有 Origin: cat.com,只是伺服器沒去驗→ 存+演練
- P檢查自己專案的 session cookie 有沒有設 SameSite→ 練習
AI 補充作者為了讓推理成立,說「伺服器分不出請求從哪來」。但畫面上的開發者工具其實反駁了他一半:這個從 cat.com(影片裡的 attacker.com)送往 vulnerable.com/delete_my_account 的請求,標頭裡明明白白寫著 `Origin: http://cat.com` 與 `Referer: http://cat.com/`。也就是說,伺服器**看得到**來源,只是預設不會去看。這正是除了 token 之外的另一條防線:後端檢查 Origin/Referer 是否為自家網域。作者略過了這一步,但它值得補上,因為它說明 CSRF 的根本問題不是「資訊不存在」,而是「沒有人去驗」。 畫面還洩漏了另外兩個之後會用上的細節:Cookie 標頭裡那串被紅框圈起來的 `SessionID=...`——這就是「自動附帶」的實體;以及 `Content-Type: application/x-www-form-urlencoded` 與底下的 `Form Data: delete: 1`——表單送出的資料就是這種 key=value 格式,這個限制稍後會變成攻擊者最大的麻煩。 最後補一句作者含糊帶過的「除非有設一些旗標」:那個旗標叫 SameSite,而它在 2019 年之後從「選配」變成了「預設」,是這支影片最需要更新的一點(見本段勘誤)。
術語:Origin headerReferer headerSameSite cookie
「on every single request the cookies are automatically sent to the website regardless of your current domain」
6. 防禦 anti-CSRF token 與「沒破壞任何規則」 4:08–5:14
防禦方式是後端隨機產生 anti-CSRF token,每次請求都要帶並驗證;攻擊者網站不知道 token 就送不出有效請求。作者接著回頭解釋他為何要先講同源政策:CSRF 並沒有繞過它——請求本來就送得出去,只是讀不到回應,而讀不到並不妨礙刪除已經發生。

推理因為上一段已經證明「兩個請求長得一模一樣」,防禦的方向就被推導出來了:既然無法從請求本身分辨來源,那就在請求裡加一個攻擊者拿不到的東西。這就是 anti-CSRF token——後端隨機產生、送到自家頁面上、每次請求帶著、伺服器逐次驗證;攻擊者的網站因為讀不到你的頁面內容(同源政策擋讀),也就無從得知這個 token。注意這個防禦之所以有效,靠的正是第三段那條「讀不到」的規則——同一條規則在攻擊時無害、在防禦時致命。 接著作者主動回頭解釋自己為什麼要花時間講同源政策:他要澄清 CSRF **沒有**繞過它。請求本來就送得出去(同源政策不管),回應確實讀不到(同源政策有管),一條規則都沒被打破。這是全片最重要的一次觀念校正。
- CAnti-CSRF token:後端隨機產生、綁定 session,攻擊者讀不到也猜不到→ 畫地圖
- Aanti-CSRF token 的安全性等於密碼強度:能被猜到就等於沒設防→ 批判類比
- P檢查自己專案的 CSRF token 是不是隨機、且綁定使用者 session→ 練習
AI 補充作者說 token 讓「別的網站沒辦法發出跨來源請求」,這句話容易誤導:攻擊者當然還是能發請求,只是那個請求會因為缺少或帶錯 token 而被伺服器拒絕。防線在伺服器的驗證,不在瀏覽器。 他也沒說 token 為什麼非得「隨機」不可——因為 token 的整個安全性就等於「攻擊者猜不到」。可預測的 token(時間戳、帳號雜湊、遞增序號)等於沒有防護,這一點作者到最後一段才會回頭提。同理,token 必須綁定使用者的 session:一個全站共用的固定 token,攻擊者只要自己註冊一個帳號就拿到手了。 畫面把架構切成 Front End 與 Back End 兩欄,暗示了這個機制的分工:token 誕生於後端(攻擊者碰不到的地方),被交付到前端頁面裡(同源政策保護的地方),再隨請求送回後端比對。攻擊者站在這個迴圈之外,兩端都伸不進去。 最後把「沒有繞過同源政策」翻成一句更白的話:同源政策從來不是為了保護「動作」,它保護的是「資料」。CSRF 偷的不是資料,是動作——它攻擊的正好是同源政策不負責的那一塊,所以兩者從頭到尾沒有衝突。
7. 攻擊頁的程式碼長什麼樣 5:14–5:56
看 attacker.com 的原始碼:一個 form,action 指向有漏洞應用程式的 delete 端點,method 設成 POST,再加一小段 JavaScript 自動送出,所以受害者完全不需要點任何東西。除了 form,也可以直接用 JavaScript 發 Ajax 請求在背景完成。

推理因為原理已經站得住腳,作者接著要證明它有多廉價——這是漏洞教學常見的收尾動作:把抽象的威脅換算成幾行程式碼。畫面上只有兩塊:一個 form,action 指向 http://vulnerable.com/delete_my_account、method 是 POST、裡面一個 `<input type="hidden" name="delete" value="1">`;再加一行 `document.getElementById('csrf_form').submit()`。作者強調那行 JavaScript 的作用是「自動送出,所以完全不需要使用者互動」——正好補上第四段那個「你什麼都沒按」的空缺。最後他補一句:除了 form,也可以直接用 JavaScript 發 Ajax 請求在背景做完。
AI 補充這段程式碼裡最值得注意的其實是它**沒有**用到什麼:沒有漏洞利用碼、沒有繞過任何檢查、沒有一行不合法的 HTML。它是一份任何初學者第一週就會寫的表單,只是 action 指向了別人家。這呼應第四段說過的——被偽造的不是內容,是意圖。 `type="hidden"` 的用途也不是隱藏什麼機密,只是讓這個必要的 POST 參數不要顯示在畫面上,受害者連「這頁有個表單」都不會察覺;配合那行 `submit()`,整個過程在頁面載入的瞬間就完成了。 作者順口提的「也可以用 Ajax」在此刻看起來只是替代方案,但兩者其實有一個關鍵差異,而它會直接決定後面幾段的走向:表單送出的資料格式被瀏覽器寫死(只有三種 enctype,預設是 key=value 的那種),而 JavaScript 可以自由決定 body 的內容。作者現在不點破,因為下一段他要靠這個限制製造難題。 還有一個現實面的細節:表單送出會讓受害者的畫面整個跳到目標網站,動靜很大;因此實務上攻擊者常把這個 form 藏在一個看不見的 iframe 裡送出,或改用背景的 Ajax,讓受害者停留在原頁面上毫無所覺。
術語:HTML formhidden input
8. 難題:form 送不出 JSON 5:56–7:00
現實中很多 API 期待收到 JSON,而 HTML form 送出的 POST 資料是 key=value,不是 JSON。作者用一個 /delete-item 端點當例子,問:要怎麼用 form 把 JSON 送過去?他叫觀眾先暫停影片自己想。

推理因為上一段的攻擊建立在「目標端點收 key=value」這個好運上,作者必須誠實面對現實:現在到處都是 API,伺服器往往期待收到 JSON。他把落差擺成一道題——畫面左邊是一個帶著 `{"name":"apple"}` 的 POST 打向 /deleteItem,右邊是 apple 被刪掉;但上一段那個 form 送出的是 `name=apple` 這種 key=value,兩者對不上。他同時給了破題的方向:「如果你想一下這些 POST 參數是怎麼從 HTML 元素產生出來的,其實可以找到繞過去的方法」,然後叫觀眾暫停自己想。這是全片唯一一次留白,也是把觀眾從「聽懂」推到「會推」的轉折。
AI 補充作者留下的那個提示值得展開,因為它是下一段所有推導的支點:表單送出的 body 不是憑空產生的,而是把每個欄位的 **name** 與 **value** 用 `=` 串起來、欄位之間用 `&` 隔開。也就是說 body 的每一個字元,幾乎都直接來自攻擊者可以自由填寫的 name 與 value——瀏覽器只負責在中間插入 `=` 和 `&`。攻擊者無法控制的,就只有那些插進來的符號。 把問題精確化就變成:一個形如 `<攻擊者可控>=<攻擊者可控>` 的字串,有沒有辦法讓它同時是一段合法的 JSON?麻煩之處在於中間那個 `=` 賴著不走,而 JSON 語法裡沒有 `=` 的容身之處。這就是下一段要拆的結。 這裡還有一個前提要先講明,否則後面會覺得矛盾:這一整套「用表單拼 JSON」的把戲,只在伺服器**不檢查 Content-Type** 時才有意義——因為表單送出時 Content-Type 一定是 `application/x-www-form-urlencoded`(或另外兩種),永遠不會是 `application/json`。作者要到很後面才會揭曉這個限制;先記著這個伏筆,前後就接得上了。
9. 用表單拼出一段合法 JSON 7:00–9:05
作者觀察到送出的字串大部分由使用者控制,於是一步步把 name=apple 改造成合法 JSON:先把大括號和 JSON 內容塞進 name,剩下尾巴那段沒意義的文字則有兩招——在 Ruby 環境可以用 // 註解掉,通用的做法則是把尾巴變成一組叫 junk 的無意義 key/value。伺服器只挑出它要的欄位,junk 被忽略。



推理因為 body 的絕大部分字元都由攻擊者填的 name 與 value 提供,作者先做一次實驗確認這件事:把 itemName 設成 apple 送出,觀察到 `itemName=apple`,並指出「這個字串大部分是使用者可以控制的」。有了這個觀察,改造就成了填空題: 1. **把整段 JSON 搬進 name。** 既然 name 完全可控,就把 `{"itemName":"apple"}` 整個當成欄位名稱寫進去,value 隨便給。送出後得到 `{"itemName":"apple"}=apple`——JSON 該有的都有了,但尾巴多了 `=apple` 這截垃圾,整串因此不合法。 2. **第一招:註解掉尾巴。** 作者自問「JSON 不支援註解啊」,再自答「但在 Ruby 裡可以」,於是在結尾補兩個斜線把垃圾註解掉。他自己立刻推翻這條路:「如果它不是 Ruby 呢?」——這招依賴目標的解析器,不通用。 3. **第二招:把垃圾變成資料。** 既然等號後面的內容也可控,那就別想著刪掉等號,而是把它關進一個字串裡。做法是讓 name 停在一個尚未關閉的引號上、由 value 去補完它:`name='{"itemName":"apple","junk":"'`、`value='test"}'`,瀏覽器插入的等號就落在 junk 的字串值中間,得到 `{"itemName":"apple","junk":"=test"}`——完全合法。伺服器只挑出它要的 itemName,junk 被無視。 整段推理的模式是:不對抗限制,而是把限制納入設計。
- CJunk parameter:把瀏覽器插入的等號吞進字串值裡,拼出合法 JSON→ 畫地圖
- A把等號關進字串值裡讓 JSON 合法,跟 SQL injection 的引號技巧同一招→ 批判類比
- Ename='{"itemName":"apple","junk":"'、value='test"}' 拼出合法 JSON→ 存+演練
- Renctype 必須設成 text/plain 的原因→ 存+回想
AI 補充這裡最漂亮的一步,是作者從「刪掉等號」轉念成「讓等號變成無害的資料」。前者不可能(瀏覽器一定會插),後者只要跨過一個引號邊界就成立。這種「把不可控的字元吞進字串字面值裡」的手法在注入類漏洞中反覆出現,是很值得帶走的思考模式。 有幾個作者跳過、但實作時會卡住的細節: - **JSON 必須寫在 name 而不是 value**,因為 body 的排列順序是「name 在等號左邊」。放進 value 的話開頭的大括號前面就會多出一截 `something=`,那才是真的無藥可救。 - **不能用 `type="hidden"` 以外的形式亂放**,而且整個表單只放這一個欄位——多一個欄位就多一個 `&`,JSON 又不合法了。 - **enctype 必須設成 `text/plain`**。這是作者完全沒提、卻不可或缺的一步:預設的 urlencoded 會把大括號、引號、冒號全部百分號編碼成 `%7B`、`%22`,送到伺服器就不是 JSON 了。只有 `text/plain` 會原封不動地送出 `name=value`,這套把戲才成立。 - 也因為只有 `text/plain` 可用,這個請求的 Content-Type 一定是 `text/plain`,永遠不會是 `application/json`——所以整套攻擊只對「不檢查 Content-Type 的伺服器」有效。這正是上一段埋下的伏筆,作者接下來就要正面處理它。 順帶一提,第一張截圖(440 秒)抓在換場動畫中間,畫面上只有一條剛畫出來的分隔線,沒有內容;真正的推導從 462 秒那張的左右對照開始看即可。
術語:junk parametertext/plain enctype
「but in Ruby it's actually a thing comments are kind of allowed so you can simply add two forward slashes at the end」
10. 更簡單的做法:fetch 9:05–9:30
其實還有更省事的路:直接用 fetch 把 JSON 字串當成 body 送出去。照樣會出現同源政策的錯誤,但那只影響讀取回應——請求已經抵達伺服器,該發生的更動照樣發生。

推理因為上一段的限制全部來自「表單的 body 由瀏覽器組裝」,那只要換一個能自己決定 body 的工具,限制就整組消失。作者的畫面直接了當:標題「Simpler Way?」,底下一行 `fetch();`——用 JavaScript 發請求,把 JSON 字串原樣當成 body 送出去,不必拆解、不必收容等號、不必造 junk 欄位。 他接著處理唯一會讓人猶豫的地方:主控台會跳出同源政策的錯誤。他的回應是全片邏輯的最後一次收攏——「我們不在意,因為請求已經打到伺服器了,更動照樣會發生,跟輸出無關」。這正是第三段「擋讀不擋送」與第四段「目標是改動而非讀取」兩條線在此交會:錯誤訊息看起來像失敗,但失敗的只是「讀回應」這一步,而攻擊從來不需要那一步。
AI 補充作者示範的是概念,實際要跑起來還缺兩塊拼圖,而這兩塊都直接影響這條捷徑的可行性: - **cookie 不會自動跟著走。** `fetch` 的 `credentials` 預設是 `same-origin`,跨來源請求預設**不帶** cookie;必須明寫 `credentials: 'include'` 才會附上。這是它和表單最大的差別——表單天生帶 cookie,fetch 要自己要求。少了這一行,請求雖然送到了伺服器,卻是個未登入的請求,什麼也刪不掉。(順帶一提,這也是為什麼 `XMLHttpRequest` 時代要設 `withCredentials = true`。) - **body 的 Content-Type 決定會不會被攔下。** 用 `fetch` 送一個字串而不特別指定標頭時,瀏覽器會標上 `text/plain`,這屬於「簡單請求」,會直接送出去、只是讀不到回應——作者描述的情況就是這種。但如果為了配合 API 而標上 `application/json`,請求就升級成需要預檢的類型,瀏覽器會先送一個 OPTIONS 去問伺服器,沒得到許可就**連正式請求都不會送出**。也就是說這條捷徑的短,是建立在「不設 Content-Type」上的。 這兩點合起來剛好把故事推到下一步:到目前為止的所有做法——表單也好、fetch 也好——都只能送出 `text/plain` 之類的內容型別。如果伺服器堅持要 `application/json` 才收,前面兩招就同時失效了。
術語:request body
11. Content-Type 這道關卡與 CORS 9:30–11:24
前面兩招只在伺服器忽略 Content-Type 時有效;一旦伺服器要求 application/json,form 沒辦法加標頭,JavaScript 想加也會被瀏覽器擋下。能不能加取決於 CORS——伺服器必須在回應裡用 Access-Control-Allow-Origin 與 Access-Control-Allow-Headers 明確放行,才設得了這個標頭。

推理因為前兩招都只能送出 `text/plain` 之類的內容型別,作者現在把伺服器變嚴格:它期待 `Content-Type: application/json`,明確宣告這是 JSON 資料。於是攻擊者的兩條路同時斷掉——表單「沒辦法多設一個標頭」(enctype 只有三種、沒有自訂標頭的餘地),JavaScript 雖然設得了標頭,卻換來另一道關卡:「瀏覽器不會讓你隨便設任何標頭,除非伺服器用 CORS 明確允許」。 接著他解釋 CORS 是什麼,並強調它為什麼必要:API 通常跟呼叫它的網站不同網域,所以必須有一個正當管道讓不同來源溝通。最後給出具體條件——伺服器要在回應標頭裡回傳 `Access-Control-Allow-Origin`(你的網域)與 `Access-Control-Allow-Headers`(你想設的那個標頭),你才設得了 Content-Type,才送得出帶著 `application/json` 的 JSON CSRF。 注意這裡的推理方向反轉了:前面幾段都是攻擊者在想辦法,這一段的主導權完全在伺服器手上。攻擊能不能成立,取決於防守方自己開了多大的門。
- CCORS:讓不同來源合法溝通的機制,決定權在伺服器要不要開門→ 畫地圖
- CPreflight request:不簡單的跨來源請求先送 OPTIONS 問許可,沒回應就整個不送→ 畫地圖
- E真實漏洞常長成:伺服器把請求的 Origin 原封反射回 ACAO,還加 Allow-Credentials: true→ 存+演練
- P檢查專案的寫入型 API 有沒有嚴格檢查 Content-Type,拒收非預期型別→ 練習
- R帶 cookie 的跨來源請求還需要 Allow-Credentials→ 存+回想
AI 補充作者省略了這道關卡的運作機制,補上之後才會知道它為什麼有效: **預檢(preflight)。** 當一個跨來源請求「不簡單」——自訂了標頭,或 Content-Type 不是那三種表單型別之一——瀏覽器不會直接送出,而是先送一個 `OPTIONS` 請求去問:我可以用這個方法、帶這些標頭嗎?伺服器沒有回答許可,正式請求就**根本不會離開瀏覽器**。這一點至關重要:它不是「送出去但讀不到回應」,而是「連送都沒送」。換句話說,第三段那條「擋讀不擋送」的規則在這裡失效了,而攻擊的整套邏輯正是建立在那條規則上——這才是 Content-Type 檢查能當防線的真正原因。 **一個作者沒提的必要條件。** 就算伺服器回了 ACAO 與 ACAH,帶著 cookie 的跨來源請求還需要 `Access-Control-Allow-Credentials: true`,而且此時 ACAO **不能**是萬用字元 `*`,必須明確寫出那個來源。這道限制是刻意設計的:它讓「對全世界開放讀取」與「連同身分一起開放」無法同時成立。實務上真正的漏洞多半長成這樣——伺服器把請求送來的 Origin 原封不動反射回 ACAO,又加上 `Allow-Credentials: true`,等於對任何網站敞開大門。 **別把 CORS 想成攻擊者的工具。** 從畫面上的流向就看得出來:Content-Type 是網站送給伺服器的,而 Access-Control-* 是伺服器回給網站的——決定權自始至終在伺服器那一端。設定正確時 CORS 是把攻擊擋在門外的那道鎖;出問題的從來不是 CORS 本身,而是被設錯的 CORS。 順帶一提,「要求 Content-Type: application/json」因此成了一條相當划算的 CSRF 防線:它不需要 token、不需要維護狀態,只要 API 拒收其他型別,純表單的攻擊就完全出不了門。
術語:preflight request
「this is basically a bypass for same origin policy」
12. Flash 加 307 轉址繞過標頭限制 11:24–13:02
不靠 CORS 也有辦法:Adobe Flash 檔案不受同源政策直接管轄,可以強制指定 Content-Type,但它送出的請求不會自動帶 cookie。解法是讓 Flash 先打到自己的 307 轉址頁,307 會原封不動保留標頭與 POST body 再轉給目標網站,等於做了一次中繼,湊出帶正確標頭又帶 cookie 的 JSON CSRF。

推理因為瀏覽器不讓網頁腳本隨意設標頭,作者換了一個不歸瀏覽器同源政策直接管的執行環境:Adobe Flash 檔案有自己一套跨網域規則,所以可以強制指定 Content-Type。畫面上的流程也是這樣起頭的:受害者被引到 evil.com/flash,那個 Flash 檔裡已經寫好 `Content-Type: application/json` 與 `{"delete":1}` 的 JSON 資料,右邊站著 vulnerable.com。 但作者馬上點出這條路自帶一個致命缺陷:Flash 送出的請求**不會帶 cookie**(除非目標有 crossdomain.xml 之類的設定)。於是他手上有兩個各自殘缺的工具——Flash 能設標頭但沒有身分,瀏覽器能帶身分但不能設標頭。 他的解法是把兩者串起來:讓 Flash 不要直接打 vulnerable.com,而是先打自己架設的 307 轉址頁;307 這個狀態碼會原封不動保留原本的方法、標頭與 POST body 再轉給目標,像個中繼站。轉址由瀏覽器執行,而瀏覽器往 vulnerable.com 送請求時會照慣例自動附上 cookie——第二段那塊地基在這裡被最後一次動用。標頭來自 Flash、身分來自瀏覽器,兩個殘缺拼成一個完整的請求。
- C307 redirect:轉址時原封不動保留方法與 body,當中繼站接上瀏覽器自動附的 cookie→ 畫地圖
- AFlash 能設標頭沒 cookie,307 帶 cookie 沒標頭,兩個殘缺工具拼成完整攻擊→ 批判類比
- R為什麼一定要用 307 而不是 301/302→ 存+回想
AI 補充這一段的價值不在 Flash 本身(它早就死了,見本段勘誤),而在那個**串接**的思路:兩個各自無害的機制,因為能力互補而組成一條攻擊鏈。這種「A 有 X 沒 Y、B 有 Y 沒 X,把 A 的輸出接到 B 的輸入」的模式,是漏洞研究裡最常見的組合技,換到別的題目上一樣有效。 幾個作者略過的關鍵細節: - **為什麼一定要 307?** 常見的 301/302 在實務上會把後續請求降級成 GET 並丟掉 body,那就什麼都不剩了。307(以及永久版的 308)規範上明確要求維持原本的方法與 body 不變,這正是它能當「中繼站」的唯一理由。 - **為什麼繞過了預檢?** Flash 是在瀏覽器的 CORS 檢查之外送出這個請求的;等到瀏覽器接手處理轉址時,該送的請求已經在路上,標頭也已經定型。這是典型的「換一個執行環境來規避檢查」。 - **crossdomain.xml 是什麼?** 那是 Flash 自己的跨網域政策檔,放在目標網站根目錄,宣告哪些來源可以帶著身分存取——概念上等同 Flash 版的 CORS。作者說「除非有那個東西」,指的就是目標自己開了門的情況。 最後要強調的是:這套具體手法今天已經完全不能用了,但它示範的問題並沒有消失——只要瀏覽器裡還存在「不受主流安全模型管轄的執行環境」,同樣的組合技就會以新的形式出現。
術語:307 redirect
「flash files they can help you in this case」
13. 串接其他漏洞與現況 13:02–14:11
CSRF 可以跟其他漏洞串起來放大危害,作者自己就靠它拿到過遠端程式碼執行。他的結論是:CSRF 沒那麼常見了但仍然存在,而且就算有防護也可能被繞——token 從別的端點外洩,或 token 本身可預測。
推理因為前一段的 Flash + 307 已經示範過「兩個機制串起來」的威力,作者順勢把這個概念從技術層面放大到漏洞層面:CSRF 可以跟其他漏洞串接成更嚴重的問題,他上週就靠一個 CSRF 拿到了遠端程式碼執行(細節留在說明欄的 GitHub issue)。這是在回答「一個只能『讓動作發生』的漏洞為什麼值得在意」——因為那個動作可能是上傳檔案、改設定、觸發部署。 接著他給出對現況的判斷:CSRF 沒那麼常見了,但還在。最後補上一句對防守方的提醒,也剛好呼應他自己講過的防禦——有 CSRF 防護不等於安全,token 可能從**別的端點**外洩,或者 token 本身可預測。這兩個破口正好對應到 anti-CSRF token 賴以成立的兩個前提(攻擊者讀不到、猜不到,見第 6 段),拆掉任何一個,防線就垮了。
AI 補充作者說「沒那麼常見了」的理由在 2019 年主要是框架普及——Rails、Django、Laravel、Spring 這些框架都內建 CSRF 防護且預設開啟,開發者不必自己想就受到保護。這是「安全預設值」的勝利,比教育每一個開發者有效得多。 今天要再加上一個更大的因素:瀏覽器層級的 SameSite 預設(見第 5 段),它讓經典的跨站 POST 攻擊在預設情況下直接失效。兩者疊加之後,純粹的 CSRF 在現代應用上確實愈來愈難得手。但這不代表可以不管,剩下的縫依然明確:刻意設成 `SameSite=None` 的 cookie(跨網域嵌入的服務常常必須這樣設)、只靠 GET 就能改變狀態的端點、同站不同子網域之間的攻擊、以及採用 Bearer token 而不用 cookie、因此把防護重心整個換成 CORS 設定的 API。 他提的兩個破口也值得說得更具體。**token 從別的端點外洩**:常見情境是某個回傳使用者資料的 API 順手把 token 一起吐出來、或 token 被放進網址而經由 Referer 洩漏,攻擊者只要先讀到它,anti-CSRF token 就形同虛設——但要注意,能「讀到」通常意味著同源政策已被繞過(例如目標網站另有 XSS,或 CORS 設錯),所以這實際上仍是漏洞串接。**token 可預測**:用時間戳、遞增序號、帳號的 MD5 之類的方式產生 token,攻擊者算得出來就等於沒有防護,這也是為什麼隨機性不是實作細節而是整個機制的安全根據。 作者最後那句「保持警覺,你永遠不知道會找到什麼」其實是整支影片方法論的縮寫:他從頭到尾示範的都不是背下某個 payload,而是「找出機制的邊界條件,再問如果不成立會怎樣」。這種問法在 Flash 消失、SameSite 上線之後依然可用。
術語:predictable token
4. 總結
整支影片是一條扣得很緊的推理鏈。作者先問一個沒人會起疑的問題——一個能刪帳號的請求安全嗎——並指出伺服器其實分不出請求從哪個網站來。接著他放下兩塊地基:登入後的 cookie 會在每次請求自動附帶(伺服器靠它認人),以及同源政策只擋「讀回應」不擋「送請求」。把這兩塊相加,就得到 CSRF:你人在 attacker.com、什麼都沒按,瀏覽器仍會替它把帶著你 cookie 的請求送到 vulnerable.com,刪除因此發生在你的 session 裡。防禦順著同一條邏輯推出來:既然請求本身分辨不出來源,就加一個攻擊者拿不到的 anti-CSRF token——它之所以拿不到,正是因為同源政策擋讀。作者特別澄清 CSRF 沒有繞過同源政策,一條規則都沒被打破。後半段把難度往上加:目標端點若要 JSON,表單送的 key=value 對不上,作者示範把整段 JSON 塞進欄位名、再用一個 junk 欄位把瀏覽器插入的等號吃進字串裡;接著自己拆台,說用 fetch 直接送 body 簡單得多,主控台跳出的同源政策錯誤無關緊要,因為攻擊要的是「動作發生」不是「讀到回應」。最後拆掉共同前提:伺服器若認真檢查 Content-Type,表單加不了標頭、JavaScript 要加得看伺服器的 CORS 臉色,主導權回到防守方;除非用 Flash(能設標頭但不帶 cookie)搭配 307 轉址(由瀏覽器執行、會自動補上 cookie),兩個殘缺拼成一個完整請求。結尾把「串接」的概念從技術放大到漏洞層面:CSRF 可以串成遠端程式碼執行,而有防護也不等於安全,token 可能外洩或可預測。
勘誤總整理
確定錯誤/已過時見仁見智(取決於版本或情境)
| 段落 | 原話(transcript 逐字) | 說明 |
|---|---|---|
| 5. 拆解:兩個請求長得一模一樣 3:39 |
「on every single request the cookies are automatically sent to the website regardless of your current domain」 | 這是 2019 年的預設行為,現在已經反過來了。Chrome 從 80 版(2020 年 2 月起分批推出)開始,把沒有明確標示 SameSite 的 cookie 一律當成 SameSite=Lax,Firefox、Edge 隨後跟進。結果是:跨站的 POST 請求、iframe 內的請求、以及 fetch/Ajax 送出的請求,預設都不會再帶上 cookie——也就是說影片裡示範的那個「從 attacker.com 自動送出 POST 把帳號刪掉」的經典場景,今天在預設設定下不會成功。作者說「除非有設一些旗標」,那個旗標就是 SameSite,而它已經從選配變成預設。要讓 cookie 維持舊行為,網站必須主動寫上 SameSite=None; Secure。這不代表 CSRF 消失了:跨站的頂層 GET 導覽仍會帶 cookie,同站不同子網域之間也不受限制,而且刻意設成 SameSite=None 的網站依然完全暴露。 依據: Chrome 80(2020-02)起 SameSite=Lax by default;RFC 6265bis 將其納入規範;MDN Set-Cookie SameSite 屬性文件 |
| 12. Flash 加 307 轉址繞過標頭限制 11:50 |
「flash files they can help you in this case」 | 這一整套做法今天已經行不通。Adobe 於 2020 年 12 月 31 日終止 Flash Player 支援,2021 年 1 月 12 日起在播放器內封鎖所有內容,Chrome、Firefox、Edge、Safari 也都已移除播放能力,網頁上根本載入不了 Flash 檔案,因此也不存在「用 Flash 強設 Content-Type」這條路。作者自己在影片裡就說「大概再過一年就會完全消失」,只是實際上又多撐了一年半。307 轉址中繼的手法本身仍然有效,但目前瀏覽器裡沒有等價的一般手段可以在跨來源請求上任意設定 Content-Type——換句話說,這一段的價值已經從「可用技巧」轉為「串接思路的教材」。 依據: Adobe Flash Player EOL 公告:2020-12-31 停止支援、2021-01-12 起封鎖內容 |
| 3. 同源政策:能送不能讀 1:17 |
「the cross iframe communication is not possible due to something called us same origin policy」 | 說得太滿。同源政策擋的是「未經允許讀取對方的內容與資料」,不是所有跨 iframe 溝通。瀏覽器本來就提供 window.postMessage 讓不同來源的視窗/iframe 明確地互傳訊息,雙方各自檢查 origin 即可;此外還有 CORS、Channel Messaging 等機制。作者接下來自己也會談到 CORS 是「同源政策的合法繞道」,可見這句是為了鋪陳而做的簡化。 依據: window.postMessage 自 HTML5 起即為標準(MDN Window.postMessage),2019 年時所有主流瀏覽器皆已支援多年 |
| 9. 用表單拼出一段合法 JSON 8:01 |
「but in Ruby it's actually a thing comments are kind of allowed so you can simply add two forward slashes at the end」 | 講法容易誤導,而且不該被當成可靠的手段。JSON 規範本身完全不含註解,這裡能成立純粹是 Ruby 內建 json 函式庫的解析器實作上會略過 `//` 與 `/* */`,跟「Ruby 這個語言」無關——同樣跑在 Ruby 上但改用其他 JSON 解析器就未必吃這一套,其他語言的主流解析器(Python 的 json、Node 的 JSON.parse、Go 的 encoding/json)則一律拒絕。此外 json gem 後續版本已陸續收緊對註解與多餘字元的容忍度,所以這條路是「碰運氣的實作細節」而非通則。作者自己下一句就推翻它、改用 junk 參數,那才是真正通用的做法。 依據: RFC 8259 未定義註解語法;Ruby json gem 的註解略過屬實作行為且各版本寬鬆度不一 |
| 11. Content-Type 這道關卡與 CORS 10:25 |
「this is basically a bypass for same origin policy」 | 這個比喻容易留下錯誤印象。CORS 不是繞過同源政策,而是同源政策規範內建的例外申請流程:決定權完全在伺服器,瀏覽器仍然逐條執行檢查,沒有任何規則被跳過。作者自己補了「當然是好的那種、而且只有在伺服器明確允許時才成立」,但很多人聽完仍會把 CORS 記成「讓攻擊變容易的東西」。實際上設定正確的 CORS 是把這一段的攻擊擋在門外的那道鎖(預檢不通過,請求根本送不出去);真正造成漏洞的是設定錯誤的 CORS——例如把來源反射回 Access-Control-Allow-Origin 又同時開啟 Allow-Credentials。 依據: Fetch Standard 的 CORS protocol 章節;MDN Cross-Origin Resource Sharing |
5. 推薦三個下一步
1. 往下挖深:SameSite cookie 與現代瀏覽器的預設防線
影片停在 anti-CSRF token,但沒談瀏覽器層級的防禦。2019 年之後 Chrome 把 SameSite 預設改成 Lax,這直接動搖了影片第 5 段「cookie 跨站一定會自動附帶」的前提——這是理解 CSRF 現況最大的缺口。
YouTube 搜尋:SameSite cookie explained SameSite Lax default CSRF cookie security flags HttpOnly Secure SameSite
2. 往旁邊對照:XSS 與 SSRF——另外兩種「借用身分」的漏洞
CSRF 的本質是借用受害者瀏覽器的身分。XSS 是在受害者網域內執行程式碼(能讀回應,正好補上 CSRF 讀不到的那一半),SSRF 則是借用伺服器的身分。三者對照才看得出「信任邊界」這個共同主題。
YouTube 搜尋:XSS explained PwnFunction SSRF explained XSS vs CSRF difference
3. 往上應用:在 Web 框架裡實作與驗證 CSRF 防護
影片只說「用隨機 token」,沒說實務上怎麼產生、綁定 session、放進表單與 Ajax 標頭。動手在 Django / Rails / Express 設定一次,再用 Burp Suite 重放請求驗證它真的擋得住,才算把知識落地。
YouTube 搜尋:Django CSRF protection tutorial CSRF token implementation Express Burp Suite test CSRF vulnerability
- 相關 —Authorization vs Authentication - Whats the Difference? | Developer Concepts Explained · 那站講 session 怎麼認人,這站示範那份 session 怎麼被別的網站借用
- 接著看 →Content Security Policy explained | how to protect against Cross Site Scripting (XSS) · 同源政策擋讀不擋送;XSS 補上「在同源內執行」的另一半,CSP 是防線
- 相關 —15 Fullstack Concepts Every Frontend Should Know · HTTP 方法、API 與 endpoint 在那站建立,這站直接拿來拆解一個偽造請求