CSRF:瀏覽器自動附帶憑證,如何讓別的網站替你發出請求

文字解析 · 1 支影片 · 產生於 2026-10-01 00:54

🎧 語音解析📝 筆記✍️ 練習

🎧 語音解析 29m58s · TTS 講解與作者原聲交錯;點章節可跳。
邊聽邊看逐句講稿,點任一句從那裡開始
章節(13)

1. Outline

  1. 起點 · 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 綁定來源意圖 → 攻擊者繞過各種格式與標頭限制 → 防護本身也可能被繞。

推理鏈:每段留給下一段的線索

每一列是一段,箭頭後的字是這段留給下一段的線索。點段落標題跳到該段。

  1. 一個看起來沒問題的刪除請求 → 留下一個尚未回答的問題:伺服器既然分不出請求來自哪個網站,它到底憑什麼認定這個刪除請求是「你」發的?答案的第一塊拼圖,是登入之後那份會自己跟著跑的憑證。
  2. Cookie 會自動跟著送 → 現在「送出去的請求會自動帶著你的身分」已經成立。剩下的問題是:既然如此,惡意網站為什麼不能乾脆把你的資料整份讀走?擋住這件事的那條規則,正好也決定了 CSRF 的邊界。
  3. 同源政策:能送不能讀 → 兩塊地基都放好了:cookie 會自動送、同源政策只擋讀不擋送。把它們相加會得到什麼結果,作者要用一個具體的受害場景直接演給你看。
  4. CSRF 現場:光是點進去帳號就沒了 → 現在有了「什麼都沒按帳號卻沒了」這個結果,卻還沒有過程。下一步必須把這條路徑拆開,看看攻擊者送出的那個請求,跟你自己按下按鈕送出的那個請求,到底差在哪裡。
  5. 拆解:兩個請求長得一模一樣 → 攻擊怎麼成立已經清楚了,但兩個問題還懸著:那要怎麼防?以及——既然同源政策存在,這個攻擊到底算不算繞過了它?下一段作者兩個一起回答。
  6. 防禦 anti-CSRF token 與「沒破壞任何規則」 → 原理與防禦都講完了,但那個攻擊頁面實際上長什麼樣還是個謎——什麼都沒按就送出請求,程式碼要怎麼寫?接下來要打開 attacker.com 的原始碼。
  7. 攻擊頁的程式碼長什麼樣 → 程式碼短得令人不安,但它成立的前提是目標端點收得下 form 送出的 key=value 格式。如果目標要的是 JSON 呢——那條路馬上就走不通了。
  8. 難題:form 送不出 JSON → 題目已經出好:body 的形狀被瀏覽器寫死成「可控=可控」,而目標要的是一段合法 JSON。下一段作者會一步一步把這個等號吃掉。
  9. 用表單拼出一段合法 JSON → 拼字串的路走通了,但過程繁瑣又處處是地雷。作者接著要丟出一個讓人洩氣的對比:其實有一條短得多的捷徑。
  10. 更簡單的做法:fetch → 兩條路都通了,但它們共用同一個致命前提:伺服器不在乎 Content-Type。下一段要拆掉這個前提,看看當伺服器認真檢查標頭時,攻擊者還剩下什麼。
  11. Content-Type 這道關卡與 CORS → 門被鎖上了,而且鑰匙在防守方手裡。作者最後要示範一種完全不敲這道門的路:如果有一個不受同源政策直接管轄的東西,能自己決定標頭呢?
  12. Flash 加 307 轉址繞過標頭限制 → 技術面的攻防到此為止,攻擊者已經把能想的路都走過一遍。剩下的問題是:這些技巧在真實世界裡值多少?而防護做好了就真的安全嗎?
  13. 串接其他漏洞與現況

3. 逐段說明

Cross-Site Request Forgery (CSRF) Explained

1. 一個看起來沒問題的刪除請求 0:00–0:36

作者先丟出一個問題:一個能刪掉使用者帳號的 web 請求,這樣實作安全嗎?他自己回答「不太安全」,因為別的網站可以送出一模一樣的跨網域請求,伺服器根本分不出來源。為了把這件事講清楚,他決定先補基礎再進正題。

承上 承接前情提要裡最基本的共識:web 應用程式靠 HTTP 請求執行動作,而有些動作(例如刪帳號)不可逆。作者刻意不先報漏洞名稱,只把這個大家都覺得理所當然的畫面擺上檯面。

推理因為觀眾對「一個能刪帳號的請求」不會有戒心,作者就從這裡下手:他先問「這樣的實作安全嗎」,自答「不太安全」,再給出理由——如果另一個網站送出一模一樣的跨網域請求,伺服器要怎麼知道它從哪來?他沒有馬上回答這個問題,而是宣布要先補基礎。這個順序本身就是推理:問題必須先變得「不可迴避」,聽眾才會願意繞路去看兩塊地基。

AI 補充作者在這 36 秒裡其實藏了一個沒說破的前提:HTTP 請求本身不帶「我是誰、我從哪個頁面發出的」這種可信欄位。伺服器看到的只有方法、路徑、標頭和 body,而這些全部都可以被另一個網站原樣複製出來。Referer 與 Origin 標頭雖然會透露來源,但前者常被隱私設定或代理拿掉、後者當年並非所有情境都會帶,所以「不能只靠請求本身判斷來源」在當時是合理的出發點。也要先分清楚:這裡談的是「誰送出這個請求」(來源),不是「這個請求屬於哪個帳號」(身分)——身分那一塊瀏覽器等一下會自動幫忙補上,正是這個「自動」讓兩者被混為一談。

術語:endpoint

cross-domain request

跨網域請求

由 A 網域的頁面發往 B 網域的 HTTP 請求。

瀏覽器從來沒有禁止「發出」跨網域請求——載入圖片、字型、送出表單、嵌入 iframe,本來就都在跨網域地發請求,網頁生態少了它就無法運作。真正被限制的是「讀回應」。很多人第一次接觸這個題目時會以為瀏覽器會擋下跨網域請求,於是想不通 CSRF 為什麼成立;先把「送得出去」和「讀不讀得到」拆成兩件事,後面的推理才會順。嚴格說「跨網域」(cross-domain)和「跨來源」(cross-origin)不完全等價,後者還要比對協定與連接埠,作者接下來會補上這個差別。

相關術語: same-origin policy (被…限制讀取)

出處:第 1 段「一個看起來沒問題的刪除請求」

endpoint

端點

伺服器上一個可被請求的固定路徑,對應一個特定動作或資源。

作者整支影片都用「端點」當攻擊的靶心:只要知道路徑、方法與參數格式,任何人都能拼出打向它的請求。會改變狀態的端點(刪除、轉帳、改密碼)和只讀資料的端點在安全上分量完全不同——前者被偽造一次就造成不可逆的後果,後者被偽造了攻擊者也讀不到回應。這也是為什麼防護通常只加在會改變狀態的端點上。

相關術語: cross-domain request (被…指向)

出處:第 1 段「一個看起來沒問題的刪除請求」

留給下一段 留下一個尚未回答的問題:伺服器既然分不出請求來自哪個網站,它到底憑什麼認定這個刪除請求是「你」發的?答案的第一塊拼圖,是登入之後那份會自己跟著跑的憑證。

2. Cookie 會自動跟著送 0:36–1:06

第一個基礎:登入之後瀏覽器會存下 cookie,而只要你對那個網站發出請求,cookie 就會自動附帶出去,伺服器靠它確認你已登入。作者特別要聽眾把這句話記牢,因為整個攻擊都建立在「自動」兩個字上。

作者畫出「瀏覽器 → 應用程式,cookie 自動附帶」的示意圖,這張圖是後面所有推理的地基。
1:00 · 作者畫出「瀏覽器 → 應用程式,cookie 自動附帶」的示意圖,這張圖是後面所有推理的地基。
承上 上一段留下的問題是「伺服器憑什麼認定這個刪除請求是你發的」。這一段給出第一塊拼圖:登入後拿到的 cookie,就是伺服器用來認人的憑證。

推理因為上一段已經把「來源不可信」的疑問架好,作者這裡不急著講攻擊,先把伺服器認人的機制講白:登入 → 拿到 cookie → 存在瀏覽器 → 之後每一次請求都自動附帶。他甚至喊出「接下來這句話請仔細聽」,等於在推理鏈上打了一個記號——他知道整個 CSRF 的成立條件就藏在「自動」兩個字裡,而觀眾此刻還沒察覺這是個弱點,只覺得這是理所當然的便利。

AI 補充作者把「自動」講成一句話帶過,但值得補上它是誰在自動:不是你的 JavaScript、也不是網頁作者,而是**瀏覽器**。瀏覽器對 cookie 的判斷依據只有目的地網域(以及 path、Secure 等屬性),完全不看「這個請求是哪個頁面發起的」。所以在瀏覽器眼中,你自己按下按鈕送出的請求,和某個惡意頁面替你送出的請求,都只是「送往 vulnerable.com 的請求」,兩者一樣有資格附上 cookie。 畫面的示意圖也把這件事畫得很誠實:一個瀏覽器視窗、一台伺服器,中間一條標著「+Cookie」的單向箭頭。圖上沒有畫出「使用者點了什麼」「頁面是哪一頁」——因為那些資訊根本不在這條流程裡。上一段的「伺服器分不出來源」與這一段的「瀏覽器不管來源照樣附 cookie」,其實是同一件事的兩端:一端不問,另一端不說。

術語:session

cookie

餅乾(瀏覽器儲存的小段資料)

伺服器交給瀏覽器保存、之後每次對該網域發請求時自動附帶的一小段資料。

cookie 之所以被拿來當登入憑證,是因為 HTTP 本身無狀態:伺服器處理完一個請求就忘了你是誰,必須靠每次請求都帶回來的東西才認得出人。瀏覽器決定要不要附上某個 cookie,看的是目的地網域與 path 等屬性,而不是發起請求的頁面在哪——這個設計就是 CSRF 的根。常見誤解是「cookie 只有在我停留在那個網站時才會送」,實際上只要請求打向那個網域,不論從哪個分頁、哪個網站發出,它都會跟著去。

相關術語: session (承載)、cross-domain request (自動附帶於)

出處:第 2 段「Cookie 會自動跟著送」

session

工作階段

伺服器端記住「這個瀏覽器已登入為某使用者」的一段狀態,通常靠 cookie 認領。

cookie 多半只是一把鑰匙(session id),真正的使用者狀態存在伺服器那邊。這個分工帶來的後果是:任何能讓那把鑰匙被送出的請求,都會被伺服器當成「本人操作」執行,即使發動者根本讀不到伺服器回了什麼。理解這一點,就能明白為什麼 CSRF 是「盲打」也依然有殺傷力——攻擊者不需要看到結果,只需要動作被執行。

相關術語: cookie (由…認領)

出處:第 2 段「Cookie 會自動跟著送」

留給下一段 現在「送出去的請求會自動帶著你的身分」已經成立。剩下的問題是:既然如此,惡意網站為什麼不能乾脆把你的資料整份讀走?擋住這件事的那條規則,正好也決定了 CSRF 的邊界。

3. 同源政策:能送不能讀 1:06–2:19

第二個基礎是同源政策。iframe 可以把別的網站嵌進來,但跨 iframe 讀資料被同源政策擋住,必須同網域、同協定、同連接埠三個條件全中才讀得到。作者強調這條規則之所以重要,正是因為 cookie 會自動送出;規則同樣適用於 Ajax。

畫面列出同源政策的三個條件(domain / schema / port)並逐條打勾,是判斷「同源」的唯一依據。
1:55 · 畫面列出同源政策的三個條件(domain / schema / port)並逐條打勾,是判斷「同源」的唯一依據。
承上 上一段問「既然 cookie 自動附帶,惡意網站為什麼不能把你的資料整份讀走」。這一段就是答案:擋住讀取的那條規則叫同源政策——而它擋的只有讀。

推理因為上一段已經讓觀眾意識到 cookie 是自動的,作者接著必須解釋為什麼世界還沒毀滅,否則推理鏈會斷。他的順序是:先用 iframe 舉例(一個網站可以把另一個網站嵌進來),再說跨 iframe 讀不到資料,因為有同源政策;然後點名它為什麼重要——「正是因為 cookie 會自動送出」。這句話把第二段直接扣回來。最後他給出判斷同源的三個條件,並補一句「這不只適用於 frame,也適用於 Ajax」,把規則從特例升級成通則。

AI 補充作者說「跨 iframe 的溝通做不到」,這句話要收窄成「跨來源『讀取對方的內容與回應』做不到」才精確;有明確設計過的溝通管道(例如 postMessage)其實是可行的,同源政策擋的是未經允許的資料存取,不是所有互動。 更關鍵的是他沒有明說、但整支影片都建立在上面的那條界線:同源政策管的是**讀回應**,不是**發請求**。請求照樣送得出去、伺服器照樣會執行、cookie 照樣會附帶,只是瀏覽器不把回應交給發起的頁面。對一個「刪除帳號」這種做完就算數的動作來說,讀不讀得到回應根本無關緊要——傷害在請求抵達的那一刻就已經造成了。 畫面上的比對也值得看一眼:同源不是比對「網址看起來像不像」,而是逐段比對協定、網域、連接埠三個欄位,全等才算同源。圖上特別註明「http 的預設連接埠是 80」,提醒你沒寫出來的部分也在比對之列——所以 http://a.com 與 https://a.com 不同源(協定不同),a.com 與 sub.a.com 也不同源(網域不同)。

same-origin policy

同源政策

瀏覽器的安全規則:只有同來源的頁面才能讀取彼此的內容與回應。

它是瀏覽器安全的地基,但它的守備範圍常被高估。它擋的是「A 來源的腳本存取 B 來源的資料」,不擋「A 來源的頁面向 B 發出請求」——因為後者是整個網頁生態的日常(圖片、CSS、表單送出都是跨來源請求)。這個「擋讀不擋送」的縫,就是 CSRF 存在的空間,也是為什麼作者要在講攻擊之前先把它講清楚:等一下他會回過頭強調 CSRF 並沒有違反同源政策。

相關術語: origin (以…為判斷單位)、cookie (不管轄其附帶)

出處:第 3 段「同源政策:能送不能讀」

origin

來源

協定、網域、連接埠三者的組合,三者全等才算同一個來源。

「來源」是瀏覽器安全的最小信任單位,比「網域」嚴格:http://a.com 與 https://a.com 不同源(協定不同),a.com 與 sub.a.com 不同源(網域不同),a.com:80 與 a.com:8080 不同源(連接埠不同)。沒寫出來的連接埠會用協定的預設值(http 是 80、https 是 443)參與比對。順帶一提,cookie 的歸屬判斷用的是另一套規則(以網域與 path 為主,不看連接埠),兩套規則不一致,正是很多跨站問題的來源。

相關術語: same-origin policy (被…比對)

出處:第 3 段「同源政策:能送不能讀」

iframe

內嵌框架

HTML 元素,可以把另一個網頁整個嵌進目前的頁面裡。

iframe 是「跨來源共存但不互通」最直觀的示範:畫面上兩個網站疊在一起,程式上卻互相讀不到對方。作者用它當同源政策的教具,是因為它讓「能載入」與「能讀取」的差別看得見。同樣的心智模型稍後會被搬到攻擊上:攻擊者的頁面能讓受害者的瀏覽器對目標網站做事,但看不到做完之後的畫面。

相關術語: same-origin policy (受…限制)

出處:第 3 段「同源政策:能送不能讀」

Ajax

非同步 JavaScript 請求

用 JavaScript 在背景發出 HTTP 請求、不重新整理頁面的做法。

Ajax 是早期用 XMLHttpRequest 發背景請求的統稱,今日多半改用 fetch。作者特別點名它,是要說明同源政策不是「iframe 專用規則」,而是套用在所有跨來源資料存取上——用腳本發請求一樣讀不到跨來源的回應。這也預告了後面的一個關鍵細節:讀不到回應不代表請求沒送到。

相關術語: same-origin policy (同樣受限於)、iframe (同類受限對象)

出處:第 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 年時所有主流瀏覽器皆已支援多年
留給下一段 兩塊地基都放好了:cookie 會自動送、同源政策只擋讀不擋送。把它們相加會得到什麼結果,作者要用一個具體的受害場景直接演給你看。

4. CSRF 現場:光是點進去帳號就沒了 2:19–3:06

作者先拆字面:CSRF 是跨站請求偽造,一個網域對另一個網域偽造請求去改動某個值。接著直接演一次:你在 vulnerable.com 有帳號,跑去點了 attacker.com,什麼都沒按,vulnerable.com 上的帳號就被刪掉了。

承上 上一段把兩塊地基放好:cookie 會自動送、同源政策只擋讀不擋送。作者說要把它們相加演給你看,這一段就是那個現場。

推理因為基礎已經備齊,作者採用「先給名字、再給災難」的節奏:他先逐字拆 CSRF——cross-site request forgery,一個網域對另一個網域「偽造」請求以改動某個值——把攻擊的定義壓縮成一句話;接著立刻切換成第二人稱敘事,讓你在 vulnerable.com 上有帳號、讓你點進 attacker.com、讓你什麼都沒按,然後宣布帳號沒了。這裡他刻意不解釋原理,因為震撼感本身就是推理的燃料:你已經知道兩塊地基,看到結果卻還沒把它們接起來,那個空缺會逼你想下去。

AI 補充「偽造」這個詞值得停一下。攻擊者偽造的不是內容——請求裡的每個位元組都可以是完全合法、伺服器天天在處理的那種;被偽造的是**意圖**:伺服器以為這是你想做的事。這正好扣回第一段那個沒被回答的問題(伺服器分不出來源),也說明為什麼防禦不可能靠「檢查請求長得對不對」。 另外要留意作者的定義裡「in order to modify some value」(為了改動某個值)不是隨口說的限定。CSRF 的目標一定是會改變狀態的動作,因為第三段已經確定攻擊者讀不到回應——偽造一個只讀資料的請求毫無收穫。這條界線之後會一直有效:所有後續的技巧(表單、fetch、Flash)都是在想辦法把一個**寫入**動作送達,沒有一個在意回應。 還有一個容易被忽略的前提:受害者必須在目標網站處於已登入狀態,攻擊才會生效。攻擊者不需要知道你的帳號密碼,甚至不知道你是誰,他只是借用你的瀏覽器;但如果你根本沒登入,那個請求送到了也不會做任何事。

術語:state-changing request

Cross-Site Request Forgery (CSRF)

跨站請求偽造

誘使受害者的瀏覽器對另一個網站發出「看起來像本人操作」的請求,藉此改動資料。

名字裡三個詞各指一件事:cross-site 是請求由別的網站發起,request 是它就是一個平凡的 HTTP 請求,forgery 是被偽造的其實是「這是使用者本人的意思」這個前提。它常被跟 XSS 搞混——XSS 是把攻擊者的腳本注入到目標網站上執行(因此能讀到目標網站的資料),CSRF 則完全在攻擊者自己的網站上執行,讀不到任何東西,只能讓動作發生。另一個常見別名是 session riding(騎乘工作階段),描述得其實更貼切:攻擊者騎的是你既有的登入狀態。

相關術語: session (騎乘)、same-origin policy (不違反)

出處:第 4 段「CSRF 現場:光是點進去帳號就沒了」

state-changing request

會改變狀態的請求

會在伺服器上造成實際變動的請求,例如刪除、轉帳、改密碼。

把端點分成「會改變狀態」與「只讀資料」兩類,是理解 CSRF 防護範圍的關鍵。攻擊者讀不到回應,所以偽造只讀請求對他沒有價值;反過來說,會改變狀態的端點只要被送達一次就已經造成後果。這也是為什麼規範建議 GET 不應該用來改變狀態——一個會刪東西的 GET 端點,光是一張 img 標籤就能觸發,連表單都不必寫。

相關術語: endpoint (一種)、Cross-Site Request Forgery (CSRF) (是…的唯一目標)

出處:第 4 段「CSRF 現場:光是點進去帳號就沒了」

留給下一段 現在有了「什麼都沒按帳號卻沒了」這個結果,卻還沒有過程。下一步必須把這條路徑拆開,看看攻擊者送出的那個請求,跟你自己按下按鈕送出的那個請求,到底差在哪裡。

5. 拆解:兩個請求長得一模一樣 3:06–4:08

作者把剛剛的災難拆成兩條路徑對照:自己按 delete 會送出到 vulnerable.com/delete-my-account,而拜訪 attacker.com 時也有一個請求打到同一個端點,只是來源不同。因為 cookie 不管你人在哪個網域都會自動附帶,這個刪除動作就在你的 session 裡完成了。

兩條請求路徑的對照圖:同一個端點、同一份 cookie、不同來源——這是整支影片推理鏈的轉折點。
3:45 · 兩條請求路徑的對照圖:同一個端點、同一份 cookie、不同來源——這是整支影片推理鏈的轉折點。
承上 上一段留下的是「結果有了、過程沒有」。這一段就是把那條路徑拆開,比對你自己按下 delete 的請求,和 attacker.com 替你送出的請求差在哪。

推理因為上一段只給了震撼沒給機制,作者這裡改用對照法:左邊是你在 vulnerable.com 按下 delete,送出 vulnerable.com/delete-my-account;右邊是你在 attacker.com 什麼都沒按,卻也有一個請求打到「完全一樣的端點」。兩條路徑唯一的差別只有發起的網域。然後他喊出那句回扣——「現在回想我剛剛說過的:每一個請求,cookie 都會自動送到那個網站,不管你現在人在哪個網域」。到這裡第二段的地基被正式領用:不是攻擊者偷到了你的 cookie,而是瀏覽器主動幫他附上。所以刪除是在「你的 session 裡」發生的,伺服器沒有被騙、它只是照規矩辦事。

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

Origin header

來源標頭

瀏覽器在跨來源請求上自動附加的標頭,標明這個請求是從哪個來源發起的。

Origin 只寫到協定+網域+連接埠,不含路徑與查詢字串,所以比 Referer 少洩漏隱私,也因此更常被保留下來。它是由瀏覽器填寫、網頁腳本無法竄改的,這使它成為判斷來源的可靠依據——後端只要拒絕 Origin 不是自家網域的寫入請求,就擋掉了絕大多數 CSRF。要注意的是同源的請求有時不會帶 Origin(早期瀏覽器的同源 GET/POST),所以檢查邏輯要處理「沒有 Origin」的情況,通常搭配 Referer 一起看。

相關術語: origin (以…為內容)、Referer header (同類但更精簡)

出處:第 5 段「拆解:兩個請求長得一模一樣」

Referer header

來源頁面標頭

標明「這個請求是從哪一個網址的頁面發出的」的標頭(拼字沿用歷史上的錯字)。

Referer 比 Origin 詳細,會帶上完整網址,也因此常被隱私設定、瀏覽器的 Referrer-Policy 或企業代理裁剪甚至整個拿掉。這種「有時候會不見」的特性讓它不能單獨當防線——如果程式寫成「沒有 Referer 就放行」,攻擊者只要想辦法讓它消失就繞過去了。實務上它是 Origin 的備援,不是替代品。

相關術語: Origin header (備援於)

出處:第 5 段「拆解:兩個請求長得一模一樣」

SameSite cookie

同站 cookie 屬性

cookie 的一個屬性,決定它要不要跟著跨站請求一起送出。

值有三種:`Strict` 完全不跟跨站請求走;`Lax` 只在頂層導覽的 GET(例如點連結跳過去)時附帶,跨站的 POST、iframe、Ajax 一律不帶;`None` 維持舊行為,任何跨站請求都附帶,但必須同時標 `Secure`。這正是作者口中「我們今天不談的那些旗標」。2020 年之後主流瀏覽器把沒寫 SameSite 的 cookie 一律當成 `Lax`,等於在瀏覽器層面替所有網站打上一層 CSRF 防護——影片中「cookie 一定會自動送」的前提,從此不再是預設值。它仍不是萬靈丹:跨站的 GET 導覽依然會帶 cookie,同站不同子網域之間也不受它限制。

相關術語: cookie (限制…的附帶)、Cross-Site Request Forgery (CSRF) (預設緩解)

出處:第 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 屬性文件
留給下一段 攻擊怎麼成立已經清楚了,但兩個問題還懸著:那要怎麼防?以及——既然同源政策存在,這個攻擊到底算不算繞過了它?下一段作者兩個一起回答。

6. 防禦 anti-CSRF token 與「沒破壞任何規則」 4:08–5:14

防禦方式是後端隨機產生 anti-CSRF token,每次請求都要帶並驗證;攻擊者網站不知道 token 就送不出有效請求。作者接著回頭解釋他為何要先講同源政策:CSRF 並沒有繞過它——請求本來就送得出去,只是讀不到回應,而讀不到並不妨礙刪除已經發生。

anti-CSRF token 的產生與驗證流程圖,說明「攻擊者拿不到 token」這個防線長什麼樣。
4:32 · anti-CSRF token 的產生與驗證流程圖,說明「攻擊者拿不到 token」這個防線長什麼樣。
承上 上一段留下兩個懸而未答的問題:這要怎麼防?以及這算不算繞過同源政策?作者在這一段一次回答兩個。

推理因為上一段已經證明「兩個請求長得一模一樣」,防禦的方向就被推導出來了:既然無法從請求本身分辨來源,那就在請求裡加一個攻擊者拿不到的東西。這就是 anti-CSRF token——後端隨機產生、送到自家頁面上、每次請求帶著、伺服器逐次驗證;攻擊者的網站因為讀不到你的頁面內容(同源政策擋讀),也就無從得知這個 token。注意這個防禦之所以有效,靠的正是第三段那條「讀不到」的規則——同一條規則在攻擊時無害、在防禦時致命。 接著作者主動回頭解釋自己為什麼要花時間講同源政策:他要澄清 CSRF **沒有**繞過它。請求本來就送得出去(同源政策不管),回應確實讀不到(同源政策有管),一條規則都沒被打破。這是全片最重要的一次觀念校正。

AI 補充作者說 token 讓「別的網站沒辦法發出跨來源請求」,這句話容易誤導:攻擊者當然還是能發請求,只是那個請求會因為缺少或帶錯 token 而被伺服器拒絕。防線在伺服器的驗證,不在瀏覽器。 他也沒說 token 為什麼非得「隨機」不可——因為 token 的整個安全性就等於「攻擊者猜不到」。可預測的 token(時間戳、帳號雜湊、遞增序號)等於沒有防護,這一點作者到最後一段才會回頭提。同理,token 必須綁定使用者的 session:一個全站共用的固定 token,攻擊者只要自己註冊一個帳號就拿到手了。 畫面把架構切成 Front End 與 Back End 兩欄,暗示了這個機制的分工:token 誕生於後端(攻擊者碰不到的地方),被交付到前端頁面裡(同源政策保護的地方),再隨請求送回後端比對。攻擊者站在這個迴圈之外,兩端都伸不進去。 最後把「沒有繞過同源政策」翻成一句更白的話:同源政策從來不是為了保護「動作」,它保護的是「資料」。CSRF 偷的不是資料,是動作——它攻擊的正好是同源政策不負責的那一塊,所以兩者從頭到尾沒有衝突。

anti-CSRF token

防跨站請求偽造權杖

後端隨機產生、嵌在自家頁面裡、每次寫入請求都要帶回來驗證的一次性字串。

它的原理是「加入一個只有真正的頁面才知道的秘密」:攻擊者能偽造請求的每一個欄位,唯獨偽造不出這個隨機值,因為要拿到它就得讀你的頁面,而那被同源政策擋住了。常見的實作有兩種:synchronizer token(後端存一份、比對送回來的那份,最穩但要維護狀態)與 double submit cookie(同一個隨機值同時放進 cookie 和表單欄位,比對兩者是否一致,不需存狀態但對子網域攻擊較脆弱)。實務上它必須綁定 session、夠隨機、且只保護會改變狀態的請求;把 token 放進 URL 查詢字串則是常見錯誤,會經由 Referer、瀏覽紀錄與伺服器日誌外洩。

相關術語: same-origin policy (安全性倚賴)、SameSite cookie (互補防線)、state-changing request (保護對象)

出處:第 6 段「防禦 anti-CSRF token 與「沒破壞任何規則」」

留給下一段 原理與防禦都講完了,但那個攻擊頁面實際上長什麼樣還是個謎——什麼都沒按就送出請求,程式碼要怎麼寫?接下來要打開 attacker.com 的原始碼。

7. 攻擊頁的程式碼長什麼樣 5:14–5:56

看 attacker.com 的原始碼:一個 form,action 指向有漏洞應用程式的 delete 端點,method 設成 POST,再加一小段 JavaScript 自動送出,所以受害者完全不需要點任何東西。除了 form,也可以直接用 JavaScript 發 Ajax 請求在背景完成。

攻擊頁的 HTML:form 的 action / method 與自動送出的那行 JS,看了才知道「不需要使用者互動」是怎麼做到的。
5:32 · 攻擊頁的 HTML:form 的 action / method 與自動送出的那行 JS,看了才知道「不需要使用者互動」是怎麼做到的。
承上 上一段留下的謎是「什麼都沒按,請求是怎麼送出去的」。作者這裡直接打開 attacker.com 的原始碼,把那幾行揭曉。

推理因為原理已經站得住腳,作者接著要證明它有多廉價——這是漏洞教學常見的收尾動作:把抽象的威脅換算成幾行程式碼。畫面上只有兩塊:一個 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

HTML form

HTML 表單

用 action 指定目的地、method 指定方法,把欄位內容打包送出的 HTML 元素。

form 是 CSRF 最經典的載具,原因有三:它天生就能跨網域送出(同源政策不管發請求)、它不需要 JavaScript 也能運作(連 JS 被停用的環境都躲不掉)、而且它送出的請求跟真實使用者操作產生的請求在伺服器眼中毫無差別。它的限制同樣重要:method 只能是 GET 或 POST(不能是 PUT/DELETE),enctype 只能是 urlencoded、multipart 或 plain text,而且無法自訂任何 HTTP 標頭——這三個限制決定了攻擊者接下來要傷多少腦筋。

相關術語: state-changing request (用來送出)、Ajax (可替代方案)

出處:第 7 段「攻擊頁的程式碼長什麼樣」

hidden input

隱藏欄位

`type="hidden"` 的表單欄位,不顯示在畫面上但照樣隨表單送出。

它原本的用途是攜帶頁面狀態(例如商品 id、頁碼),而 anti-CSRF token 通常也放在這裡。在攻擊頁裡它扮演相反的角色:讓必要的 POST 參數(這裡是 delete=1)悄悄夾帶出去,畫面上一片空白。要注意「hidden」只是不渲染,不是加密也不是保護——任何人按下檢視原始碼都看得到,所以真正的秘密不該只靠它藏。

相關術語: HTML form (屬於)、anti-CSRF token (常見存放處)

出處:第 7 段「攻擊頁的程式碼長什麼樣」

留給下一段 程式碼短得令人不安,但它成立的前提是目標端點收得下 form 送出的 key=value 格式。如果目標要的是 JSON 呢——那條路馬上就走不通了。

8. 難題:form 送不出 JSON 5:56–7:00

現實中很多 API 期待收到 JSON,而 HTML form 送出的 POST 資料是 key=value,不是 JSON。作者用一個 /delete-item 端點當例子,問:要怎麼用 form 把 JSON 送過去?他叫觀眾先暫停影片自己想。

畫面同時秀出目標端點期待的 JSON 與 form 實際送出的 key=value,兩者的落差就是接下來要解的題。
6:40 · 畫面同時秀出目標端點期待的 JSON 與 form 實際送出的 key=value,兩者的落差就是接下來要解的題。
承上 上一段的攻擊頁靠 form 送出 key=value 就成功了,作者也提醒過那是「簡單但不是每次都這樣」。這一段就把那個「不是每次」變成具體的障礙。

推理因為上一段的攻擊建立在「目標端點收 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`。作者要到很後面才會揭曉這個限制;先記著這個伏筆,前後就接得上了。

JSON

JavaScript 物件表示法

用大括號、引號與冒號表示鍵值結構的純文字資料格式。

JSON 的語法極簡也極嚴格:字串一定要雙引號、鍵與值之間是冒號、成員之間是逗號、標準規範裡沒有註解也不容許多餘的字元。正是這份嚴格讓下一段的挑戰成立——攻擊者拼出來的字串只要多一個字元不合法,整份就會被解析器拒絕。但實務上各家解析器對規範的寬鬆程度不一(有些容忍註解、有些容忍尾隨逗號),這些差異本身就是攻擊面。

相關術語: API (常用格式)、application/x-www-form-urlencoded (對照組)

出處:第 8 段「難題:form 送不出 JSON」

API

應用程式介面

供程式呼叫的伺服器端點集合,通常以 JSON 收送資料。

作者說「現在到處都是 API,所以你很常會遇到帶著 JSON 的 CSRF」,這句話點出的是一個架構轉向:前後端分離之後,寫入動作不再由傳統表單送出,而是由前端 JavaScript 打 JSON API。這對攻擊者是雙面刃——一方面表單那套載具不再直接適用(要多繞好幾道),另一方面 API 常被放在不同網域,因此不得不開啟 CORS,反而製造出新的機會。

相關術語: endpoint (由…組成)、JSON (常以…傳輸)

出處:第 8 段「難題:form 送不出 JSON」

application/x-www-form-urlencoded

表單網址編碼格式

表單送出 POST 時的預設編碼:`name=value`,多個欄位用 `&` 串接。

這是瀏覽器替你決定的格式,網頁作者只能在三種 enctype 之間選(urlencoded、multipart/form-data、text/plain),沒有 JSON 這個選項。body 的組成方式很機械:欄位的 name、一個 `=`、欄位的 value,欄位之間插 `&`,特殊字元做百分號編碼。攻擊者能完全控制 name 與 value,卻無法拿掉中間那個 `=`——下一段的整套技巧就是繞著這個字元打轉。

相關術語: HTML form (的預設編碼)、JSON (格式不相容)

出處:第 8 段「難題:form 送不出 JSON」

留給下一段 題目已經出好:body 的形狀被瀏覽器寫死成「可控=可控」,而目標要的是一段合法 JSON。下一段作者會一步一步把這個等號吃掉。

9. 用表單拼出一段合法 JSON 7:00–9:05

作者觀察到送出的字串大部分由使用者控制,於是一步步把 name=apple 改造成合法 JSON:先把大括號和 JSON 內容塞進 name,剩下尾巴那段沒意義的文字則有兩招——在 Ruby 環境可以用 // 註解掉,通用的做法則是把尾巴變成一組叫 junk 的無意義 key/value。伺服器只挑出它要的欄位,junk 被忽略。

把 name 設成 apple 後實際送出的字串,標出哪些部分由攻擊者控制——推導的起點。
7:20 · 把 name 設成 apple 後實際送出的字串,標出哪些部分由攻擊者控制——推導的起點。
把 JSON 塞進 name 之後的中間狀態:已經很像 JSON,但尾巴多了一截,讓人看見還差什麼。
7:42 · 把 JSON 塞進 name 之後的中間狀態:已經很像 JSON,但尾巴多了一截,讓人看見還差什麼。
加上 junk key/value 後的最終合法 JSON,等號如何被吃進字串裡,只有看圖才看得懂。
8:38 · 加上 junk key/value 後的最終合法 JSON,等號如何被吃進字串裡,只有看圖才看得懂。
承上 上一段把題目定成「body 的形狀被瀏覽器寫死成『可控=可控』,而目標要的是一段合法 JSON」。這一段就是把那個等號一步一步吃掉的過程。

推理因為 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 被無視。 整段推理的模式是:不對抗限制,而是把限制納入設計。

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

junk parameter

填充參數

為了讓格式合法而塞進去、伺服器根本不會用到的一組鍵值。

作者口中的 junk 就是這個手法:不去對抗那些自己無法移除的字元,而是額外造一個欄位把它們收容進去,讓整份資料重新變得合法。它能生效有兩個前提:JSON 解析器允許出現未預期的鍵(絕大多數都允許,因為要向前相容),以及伺服器只挑自己認得的欄位。反過來說,採用嚴格 schema 驗證、遇到未知欄位就拒絕的 API,光是這一點就擋掉了這套技巧。

相關術語: JSON (用來修補…語法)、application/x-www-form-urlencoded (收容其等號)

出處:第 9 段「用表單拼出一段合法 JSON」

text/plain enctype

純文字表單編碼

表單的第三種送出編碼,把 `name=value` 原樣送出、不做百分號編碼。

表單只能在 `application/x-www-form-urlencoded`、`multipart/form-data`、`text/plain` 三種編碼之間選,其中只有 `text/plain` 會把大括號與引號原封不動地寫進 body——這是「用表單送 JSON」在實作上唯一可行的路,作者示範時略過了這一步。它同時也是這套攻擊的天花板:Content-Type 只能是這三種,永遠湊不出 `application/json`。反過來看,對伺服器而言這是一條便宜的防線——凡是寫入用的 JSON API,只接受 `application/json` 就能擋掉純表單的 CSRF。

相關術語: application/x-www-form-urlencoded (同類另一選項)、HTML form (的屬性)

出處:第 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 的註解略過屬實作行為且各版本寬鬆度不一
留給下一段 拼字串的路走通了,但過程繁瑣又處處是地雷。作者接著要丟出一個讓人洩氣的對比:其實有一條短得多的捷徑。

10. 更簡單的做法:fetch 9:05–9:30

其實還有更省事的路:直接用 fetch 把 JSON 字串當成 body 送出去。照樣會出現同源政策的錯誤,但那只影響讀取回應——請求已經抵達伺服器,該發生的更動照樣發生。

fetch 送 JSON 的幾行程式碼,對照前面拼字串的辛苦,才看得出這條捷徑有多短。
9:15 · fetch 送 JSON 的幾行程式碼,對照前面拼字串的辛苦,才看得出這條捷徑有多短。
承上 上一段用表單一路拼出合法 JSON,過程繁瑣又處處是地雷。作者在這裡丟出那個讓人洩氣的對比:其實有一條短得多的捷徑。

推理因為上一段的限制全部來自「表單的 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

fetch

瀏覽器內建的請求 API

JavaScript 用來發出 HTTP 請求的現代介面,可自訂方法、標頭與 body。

相對於表單,fetch 讓攻擊者拿回了 body 的完整控制權——這正是它能一行解決上一段整套拼字串把戲的原因。但它換來的自由是有代價的:跨來源時預設不帶 cookie(要 `credentials: 'include'`),而且一旦自訂了標頭或使用非簡單的內容型別,就會觸發預檢,反而比表單更容易被擋。所以在 CSRF 的世界裡,fetch 不是「更強的表單」,而是「另一組權衡」:表單笨拙但天生帶身分,fetch 靈活但要自己爭取。

相關術語: Ajax (的現代形式)、HTML form (對照組)

出處:第 10 段「更簡單的做法:fetch」

request body

請求主體

POST 之類的請求裡實際攜帶的資料內容。

整段 CSRF 的攻防其實可以濃縮成一句話:誰決定 body 的每一個位元組。表單的 body 由瀏覽器依欄位機械地組裝,攻擊者只能間接操控;fetch 的 body 由腳本直接指定,想寫什麼就是什麼。伺服器則透過 Content-Type 標頭得知該怎麼解讀這串位元組——標頭與 body 是否相符,正好成為下一段防守方的著力點。

相關術語: JSON (可承載)、fetch (由…指定)

出處:第 10 段「更簡單的做法:fetch」

留給下一段 兩條路都通了,但它們共用同一個致命前提:伺服器不在乎 Content-Type。下一段要拆掉這個前提,看看當伺服器認真檢查標頭時,攻擊者還剩下什麼。

11. Content-Type 這道關卡與 CORS 9:30–11:24

前面兩招只在伺服器忽略 Content-Type 時有效;一旦伺服器要求 application/json,form 沒辦法加標頭,JavaScript 想加也會被瀏覽器擋下。能不能加取決於 CORS——伺服器必須在回應裡用 Access-Control-Allow-Origin 與 Access-Control-Allow-Headers 明確放行,才設得了這個標頭。

Access-Control-Allow-Origin 與 Access-Control-Allow-Headers 兩個回應標頭的實際樣子,決定攻擊行不行得通。
11:10 · Access-Control-Allow-Origin 與 Access-Control-Allow-Headers 兩個回應標頭的實際樣子,決定攻擊行不行得通。
承上 上一段結束在「表單和 fetch 兩條路共用同一個致命前提:伺服器不在乎 Content-Type」。這一段就把那個前提拆掉。

推理因為前兩招都只能送出 `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。 注意這裡的推理方向反轉了:前面幾段都是攻擊者在想辦法,這一段的主導權完全在伺服器手上。攻擊能不能成立,取決於防守方自己開了多大的門。

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

Content-Type

內容型別標頭

告訴接收端「這個 body 該當成什麼格式解讀」的 HTTP 標頭。

它看似只是格式宣告,卻在 CSRF 裡意外地成為一道安全邊界,原因是瀏覽器對它做了硬性限制:表單只能送出三種型別,而任何超出這三種的值都會觸發預檢。於是「伺服器只接受 application/json」這個純粹的格式要求,順帶擋掉了所有無法通過預檢的跨站請求。這是少數「為了正確性而做的設計恰好帶來安全性」的例子——但也因此不該被當成唯一防線:它擋不住同源的攻擊,也擋不住伺服器自己開放了 CORS 的情況。

相關術語: request body (描述)、preflight request (可觸發)

出處:第 11 段「Content-Type 這道關卡與 CORS」

CORS

跨來源資源共享

由伺服器在回應標頭中明示放行,讓特定來源得以進行跨來源存取的機制。

它常被說成「同源政策的繞道」,但更精確的說法是「同源政策留下的正式申請窗口」:規則沒有被破壞,只是伺服器主動宣布哪些來源可以例外。它會存在,是因為前後端分離與 API 化之後,跨網域溝通變成日常需求,總不能全靠 iframe 與 JSONP 之類的取巧手法。要注意它保護的對象是**瀏覽器裡的使用者**,不是伺服器——伺服器端的 curl、後端程式從來不受 CORS 影響,所以「開了 CORS 才被打」的說法只在瀏覽器情境成立。

相關術語: same-origin policy (的例外機制)、Access-Control-Allow-Origin (透過…宣告)

出處:第 11 段「Content-Type 這道關卡與 CORS」

preflight request

預檢請求

瀏覽器在送出「不簡單」的跨來源請求前,先發一個 OPTIONS 去徵詢伺服器許可。

這是 CORS 最容易被忽略、卻對 CSRF 影響最大的一環。預檢不通過時,正式請求**完全不會送出**——不是送出後讀不到回應,而是連伺服器都沒碰到。這打破了 CSRF 賴以生存的「擋讀不擋送」前提,也是為什麼「自訂一個標頭」本身就是一種有效的 CSRF 防護(例如要求每個寫入請求都帶 `X-Requested-With`):攻擊者的頁面加不了這個標頭,加了就會卡在預檢。反過來說,凡是不觸發預檢的請求(表單送出、簡單的 GET/POST)都沒有這層保護。

相關術語: CORS (的第一步)、Content-Type (由…觸發)

出處:第 11 段「Content-Type 這道關卡與 CORS」

Access-Control-Allow-Origin

允許來源標頭

伺服器在回應中宣告「哪個來源可以存取這份資源」的標頭。

值可以是單一來源或萬用字元 `*`,但兩者的意義差很多:`*` 代表「公開資料,任何人都能讀」,此時瀏覽器會拒絕附帶 cookie 的請求;要讓帶身分的請求成立,必須寫出具體來源並搭配 `Access-Control-Allow-Credentials: true`。最常見的錯誤實作是把請求送來的 Origin 原樣反射回去再開啟 credentials——那等於對所有網站開放,是實戰中很常被抓到的高危設定。

相關術語: CORS (的核心標頭)、Origin header (回應)

出處:第 11 段「Content-Type 這道關卡與 CORS」

Access-Control-Allow-Headers

允許標頭清單

伺服器在預檢回應中列出「跨來源請求可以自訂哪些標頭」。

它是預檢問答裡的答覆欄:瀏覽器問「我可以帶 Content-Type: application/json 嗎」,伺服器必須在這個標頭裡列出 Content-Type 才算放行。這也是為什麼「要求一個自訂標頭」能當防護——只要伺服器不把它列進來,攻擊者就永遠加不上去。設定時常見的偷懶做法是直接回 `*` 或把所有標頭照單全收,那等於自廢這道關卡。

相關術語: preflight request (的回應欄位)、Access-Control-Allow-Origin (同組標頭)

出處:第 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
留給下一段 門被鎖上了,而且鑰匙在防守方手裡。作者最後要示範一種完全不敲這道門的路:如果有一個不受同源政策直接管轄的東西,能自己決定標頭呢?

12. Flash 加 307 轉址繞過標頭限制 11:24–13:02

不靠 CORS 也有辦法:Adobe Flash 檔案不受同源政策直接管轄,可以強制指定 Content-Type,但它送出的請求不會自動帶 cookie。解法是讓 Flash 先打到自己的 307 轉址頁,307 會原封不動保留標頭與 POST body 再轉給目標網站,等於做了一次中繼,湊出帶正確標頭又帶 cookie 的 JSON CSRF。

Flash → 307 轉址 → vulnerable.com 的三段式流程圖,標頭與 body 怎麼被保留,看圖最快。
12:42 · Flash → 307 轉址 → vulnerable.com 的三段式流程圖,標頭與 body 怎麼被保留,看圖最快。
承上 上一段結束在「門被鎖上了,鑰匙在防守方手裡」,並問有沒有東西不受同源政策直接管轄、能自己決定標頭。這一段的答案是 Flash。

推理因為瀏覽器不讓網頁腳本隨意設標頭,作者換了一個不歸瀏覽器同源政策直接管的執行環境: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、身分來自瀏覽器,兩個殘缺拼成一個完整的請求。

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

Adobe Flash

Adobe Flash 播放器

曾經廣泛安裝的瀏覽器外掛執行環境,有自己一套跨網域規則。

它之所以在 web 安全史上留下大量案例,正是因為它是「瀏覽器裡的第二個執行環境」:擁有自己的網路堆疊與跨網域政策,卻共用同一個瀏覽器與同一批 cookie。兩套安全模型並存的縫隙產生過無數繞過手法,這一段的強設 Content-Type 只是其中之一。Adobe 已於 2020 年底終止支援,主流瀏覽器隨即封鎖並移除播放能力,所以這段示範今天只有歷史與思路上的價值。

相關術語: same-origin policy (不直接受管)、crossdomain.xml (以…為政策檔)

出處:第 12 段「Flash 加 307 轉址繞過標頭限制」

crossdomain.xml

Flash 跨網域政策檔

放在網站根目錄、宣告哪些來源的 Flash 可以存取本站資源的設定檔。

它是 Flash 版的 CORS,但預設值與寫法都比 CORS 粗糙——歷史上大量網站直接寫成允許所有來源(`domain="*"`),等於對全世界的 Flash 開放帶身分存取。作者提到「除非有 crossdomain.xml 之類的東西」,講的就是這種目標自己敞開大門、連 307 中繼都不必繞的情況。這個檔案至今偶爾仍留在老網站上,掃描時看到它多半意味著一段沒清乾淨的歷史。

相關術語: Adobe Flash (服務於)、CORS (功能相當)

出處:第 12 段「Flash 加 307 轉址繞過標頭限制」

307 redirect

307 暫時轉址

要求瀏覽器改向新網址重送、且方法與 body 必須維持不變的 HTTP 轉址。

一般人對轉址的印象來自 301/302,而那兩者在實務上常被瀏覽器降級成 GET 並丟棄 body——這在多數情境無傷大雅,卻讓它們無法當攻擊的中繼。307 與 308 在規範上明文禁止這種改寫,於是成為「原封轉送一個 POST」的唯一手段。這個特性在正常開發裡是為了保證 API 轉址的正確性,被拿來當中繼站則是典型的能力誤用。

相關術語: Content-Type (原樣保留)、cookie (轉送時附帶)

出處:第 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 起封鎖內容
留給下一段 技術面的攻防到此為止,攻擊者已經把能想的路都走過一遍。剩下的問題是:這些技巧在真實世界裡值多少?而防護做好了就真的安全嗎?

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

vulnerability chaining

漏洞串接

把數個單獨看來危害有限的漏洞接起來,組成一個嚴重得多的攻擊。

資安評估最容易失準的地方,就是逐一評估每個漏洞的嚴重度:一個「只能讓已登入使用者誤觸某個動作」的 CSRF 看起來是中低危,但如果那個動作是上傳檔案或改寫設定,串接後就成了完整的伺服器接管。作者用 Flash 補標頭、307 補 cookie 的組合示範過機制層次的串接,這裡講的則是漏洞層次的串接——同一種思路的兩個尺度。實務上寫報告時把鏈條完整演示出來,往往是說服對方認真看待的關鍵。

相關術語: Cross-Site Request Forgery (CSRF) (常作為起點)、remote code execution (可能的終點)

出處:第 13 段「串接其他漏洞與現況」

remote code execution

遠端程式碼執行

攻擊者能讓目標伺服器執行自己指定的程式碼,通常是最高等級的危害。

它幾乎是漏洞嚴重度的天花板,因為取得執行權之後其他一切都只是時間問題。CSRF 之所以能通往 RCE,靠的不是自己有多強,而是它能觸發的那個動作——例如管理後台的「上傳外掛」「執行備份指令」「更新設定檔」。這也說明為什麼防護該以「這個端點會造成什麼後果」而不是「這個漏洞叫什麼名字」來排序。

相關術語: vulnerability chaining (常見結果)、state-changing request (經由…觸發)

出處:第 13 段「串接其他漏洞與現況」

predictable token

可預測的權杖

產生方式有規律、攻擊者能推算出來的 token,等同於沒有防護。

常見的錯誤來源包括:用時間戳或遞增序號、用使用者 id 的雜湊、用非密碼學安全的亂數產生器(例如一般的 rand()),或整站共用一個固定值。這些做法的共同問題是把「隨機」誤解成「看起來很亂」——真正的要求是攻擊者即使看過大量樣本也算不出下一個。實作上應該用密碼學安全的亂數來源產生足夠長度的值,並綁定使用者的 session。

相關術語: anti-CSRF token (的失效形式)

出處:第 13 段「串接其他漏洞與現況」

留給下一段 總結收束。從「伺服器分不出請求從哪來」出發,經過 cookie 自動附帶、同源政策擋讀不擋送、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

📄 全部影片 · 主題區: 歷史參考(已過時)