5. CSRF

在學完 XSS的借刀殺人後,接著就來談談同樣常年在 OWASP Top 10 榜單上的常客——CSRF(Cross-Site Request Forgery,跨網站請求偽造)

如果說 XSS 是駭客在你的網站裡「下毒」,那麼 CSRF 就是駭客利用使用者的信任,在背後對網站發動「跨空偷襲」

Stateless

在深入 CSRF 攻擊之前,我們必須先認識一個關於 HTTP 協定的本質:HTTP 是一個 Stateless(無狀態)的協定。

什麼是「無狀態」?簡單來說,HTTP 伺服器就像是一個「嚴重失憶症的秒忘人」。對於伺服器而言,你剛剛發出的 Request 和你現在發出的 Request,是兩個完全獨立、毫無瓜葛的事件。你前一秒才剛成功輸入帳號密碼登入,後一秒點擊「看期末成績」時,伺服器一翻兩瞪眼,當場又忘記你是誰了。為什麼要這樣設計?

因為「不記住人」其實有兩個巨大的好處:

  1. 極省資源:伺服器不需要花費珍貴的記憶體,去死記活背現在線上有哪 10 萬個使用者正在連線、每個人剛才做了什麼。
  2. 高併發超快:反正來一個請求就處理一個,處理完就兩清,這讓伺服器能以極快的速度處理海量連線。

但就算有好處這樣也超不方便阿!誰想要每點一個分頁,就要重新輸入一次帳號密碼?

為了解決這個問題,工程師發明了 Cookie(數位身份印章)。當你第一次成功登入後,伺服器會發給你一個寫著你身份的 Cookie,並蓋在你的瀏覽器上。

因為 HTTP 是 stateless 架構,所以為了讓伺服器知道你是誰、不讓你每看一個新網頁就重新登入一次,瀏覽器只要發出 Request,就會自動、順便、貼心地把你存在該網域的 Cookie 一併打包帶上。

這個體貼的自動化設計,完美解決了 Stateless 的困境,讓我們上網變得無比順暢。然而,聰明的駭客,正是盯上了這個能進入伺服器的機會…

簡介

「盜用你的身份,做你想不到的事。」,CSRF的核心原理建立在瀏覽器的自動化機制上。

假設你登入了一個網站叫做WebA,卻不小心切換分頁時,誤進了駭客做的釣魚網站(例如:www.free-cat-images.com 看可愛貓咪)。結果這個貓咪網站暗藏了一行看不見的程式碼,在你點擊圖片的瞬間,偷偷幫你發送了一個 Request 給WebA的伺服器,宣稱:「我要轉帳 10,000 元給駭客」。

瀏覽器一看到這個 Request 是發給「WebA」的,基於本能,自動把當初登入時,從伺服器那裏拿到的Cookie給蓋了上去。WebA伺服器收到後,翻開一看:「印章沒錯,是 WebA本人發的!」於是轉帳成功,你就在看貓咪圖片的同時,不知不覺被盜刷了。

在整個 CSRF 攻擊過程中,駭客從頭到尾都沒有「偷走」你的 Cookie。駭客只是借用了瀏覽器會自動帶上 Cookie 的特性,盲目地幫你發出了一個你不想要的請求。

CSRF 攻擊的三大手法

駭客要在他的壞網站上,幫你發出「跨網站請求」,通常有以下三種包裝方式:

1. 利用 <img> 標籤(針對 GET 請求)

如果銀行的轉帳系統設計得很爛,是用 GET 方法來處理敏感操作:https://bank.com/transfer?to=hacker&amount=10000

駭客只需要在他的網頁裡塞一個外表看似無辜的圖片標籤:

<img src="https://bank.com/transfer?to=hacker&amount=10000" width="0" height="0" />
Code language: HTML, XML (xml)

瀏覽器在解析這個 HTML 時,為了要把圖片載入進來,會自動對該網址發出 GET 請求(並自動帶上 bank.com 的 Cookie)。受害者連點都不用點,一進網頁直接中彈。防範這種鳥招的最好方式就是符合REST原則來實作後端API,屬於後端領域,詳請參考:

2. 利用隱藏表單 <form>(針對 POST 請求)

如果系統比較聰明,限定用 POST 請求才能轉帳呢?駭客會在網頁裡寫一個隱藏的表單,並配上一段自動執行的 JavaScript:

<form id="csrfForm" action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="hacker" />
    <input type="hidden" name="amount" value="10000" />
</form>

<script>
    // 網頁一載入,完全不通知使用者,自動把表單送出去!
    document.getElementById('csrfForm').submit();
</script>
Code language: HTML, XML (xml)

防範措施

要怎麼防範這種跨空偷襲?既然問題出在「瀏覽器會盲目地自動帶上 Cookie」,我們的防禦思路就是:讓伺服器有能力分辨,這個 Request 到底是用戶在我們官網上「心甘情願點擊」的,還是從隔壁駭客網站「隔空」送過來的。

這是現代瀏覽器全面普及的物理外掛,也是最省心、最強大的防禦。後端在核發 Cookie 的時候,可以加上 SameSite 的設定:

  • SameSite=Strict(最嚴格):只要請求不是在我們自家網站發起的(例如從 Google 點擊連結過來、或從駭客網站發起),瀏覽器一律不准帶上這枚 Cookie
  • SameSite=Lax(現代瀏覽器預設):大部分跨網站的點擊不帶 Cookie。只有在使用者進行「安全的頂級導航」(例如點擊超連結切換網頁 <a>)時才會帶上,如果是 <img> 或表單 POST 則完全封鎖。

一開啟 SameSite,駭客在隔壁分頁發動的跨站偷襲,送到銀行時就會因為「沒有帶上 Cookie」而被當場拒之門外。

CSRF Token

當使用者訪問我們自家的轉帳頁面時,Application Server 隨機生成一串只用一次的亂數密碼(CSRF Token),藏在我們前端的表單裡。

  • 當使用者點擊轉帳時,Request 同時包含 Cookie 與表單裡的 Token
  • 駭客的網站雖然可以引導瀏覽器帶上 Cookie,但因為他沒辦法偷看你家網站的 HTML(受限於瀏覽器的同源政策 Same-Origin Policy,下章會講),所以他絕對猜不到、也拿不到當下這組隨機的 Token。
  • 伺服器收到請求後,發現有 Cookie 但少了 Token,就知道這是跨站偽造的盜版請求,直接拒絕。

關鍵操作重新驗證

對於涉及財產、密碼變更的極度敏感操作,不要過度信任 Cookie。

  • 做法:在按下轉帳或修改密碼前,強制彈出視窗要求輸入「簡訊驗證碼(OTP)」「圖形驗證碼」「再次輸入舊密碼」。
  • 防禦效果:駭客在隔壁分頁只能盲目發送請求,他沒辦法幫受害者輸入手機剛收到的簡訊驗證碼。

問!

Harry 正在開發學校的「失物招領系統」。在前端他使用了 Next.js 框架,而後端 API 伺服器則是架設在另一個獨立的網域(例如:前端是 ntub.edu.tw,後端 API 是 api-ntub.com)。

在實作使用者登入功能時,Harry 面臨了兩個保存「登入憑證(Token)」的架構方案:

  • 方案 A:將 Token 存在瀏覽器的 LocalStorage 中,每次發送 API 請求時,用 JavaScript 手動把 Token 塞進 HTTP Header 的 Authorization: Bearer <Token> 裡面。
  • 方案 B:將 Token 存在 Cookie 中,並開啟 HttpOnly 防禦 XSS。

請試著用本章學到的「Stateless 與瀏覽器機制」思考並回答:

  1. 哪一個方案天生具備免疫 CSRF 攻擊的特性?為什麼?
  2. 哪一個方案雖然安全,但如果後端沒有設定好 SameSite 屬性,就會暴露出 CSRF 的隱憂
  • 答案引導:
    1. 方案 A 天生免疫 CSRF。因為 LocalStorage 裡面的資料絕對不會像 Cookie 一樣,被瀏覽器「自動、順便、貼心地一併打包帶上」。駭客在釣魚網站隔空發動請求時,瀏覽器不會幫忙帶上 Authorization 標頭,且駭客受限於同源政策(SOP,下章會講)也讀取不到 Harry 瀏覽器裡的 LocalStorage,因此 CSRF 無法成立。
    2. 方案 B 存在 CSRF 隱憂。因為方案 B 使用了 Cookie,只要是 Cookie,瀏覽器在發出請求時就會自動帶上。如果後端忘記設定 SameSite=Lax/Strict,駭客就能利用這個自動化機制發動跨站偷襲。

小結

這章節提到兩次SOP同源協定,究竟是何方神聖?我們下章揭曉!

剛學習這兩種攻擊,時常會搞混,以下整理一張表格:

漏洞名稱XSS (跨網站指令碼)CSRF (跨網站請求偽造)
攻擊本質借刀殺人,在目標網站**「下毒(塞代碼)」**。隔空打牛,利用瀏覽器機制**「冒充身份」**。
Cookie 有沒有被偷?。駭客直接把受害者的 Cookie 讀取並打包帶走。沒有。駭客從頭到尾摸不到 Cookie,只是利用瀏覽器自動帶上它。
發生的戰場100% 發生在受害者的瀏覽器內部執行。發生在跨網站的請求傳遞過程中。
核心防禦核心1. 字元跳脫(htmlentities
2. 敏感 Cookie 開啟 HttpOnly
1. Cookie 設定 SameSite=Lax/Strict
2. 引入 CSRF Token
3. 關鍵操作加驗證碼。