[網絡安全] 如何預防XSS
XSS (Cross-Site Scripting,跨站腳本攻擊) 是一種代碼注入攻擊。攻擊者通過在目標網站注入惡意腳本,使其在用戶瀏覽器中執行,從而竊取用戶敏感信息如 Cookie 和 SessionID。
CSS 在前端已經被用了,為了避免歧義用了 XSS 作為縮寫。
XSS 的本質是惡意代碼與網站正常代碼混在一起,瀏覽器無法分辨它們的可信度,最終導致惡意代碼被執行。
XSS的危害
瀏覽器無法區分惡意代碼和正常代碼,它們擁有相同的權限。惡意代碼可以讀取用戶的敏感數據,比如 cookie 和 sessionID 等等。
可能的危害如下:
- 由于惡意代碼可以獲取cookie和sessionID等數據,它可以獲取用戶的隱私數據并發送到攻擊者的服務器進行收集;
- 惡意代碼以用戶(被攻擊者)的名義,構造帶有惡意代碼的 URL 進行傳播。
XSS的分類
根據攻擊者注入惡意腳本的方式的不同,XSS 可以被分為三類:
存儲型 XSS
-
存儲區:后端數據庫
-
插入點:HTML
-
攻擊步驟:
- 攻擊者將惡意代碼提交到目標網站的數據庫。
- 用戶打開目標網站時,惡意代碼從數據庫取出并嵌入 HTML。
- 用戶瀏覽器執行惡意代碼,導致用戶數據被竊取或偽造操作被執行。
-
特點:持久性長,危害性大。
以 POST 評論和 GET 評論為例,攻擊者只要發布一條帶有惡意腳本的評論,這個攻擊就可能影響到所有瀏覽到這一條評論的其它用戶。并且這條評論是存儲在數據庫里的,假如沒有對用戶提交的評論做過濾,那么這個惡意的攻擊會一直存儲在數據庫里直到有用戶瀏覽到它,因此說它是持久的。
下圖中的步驟 1 和步驟 2 之間可能間隔很久。
案例
假如用戶發布了一條帶有惡意代碼的評論,這條評論被存儲到了數據庫里。
{ "comment": "<script>alert('XSS!');</script>" }如果網站使用服務端渲染,那么在服務端會發生模板的數據填充:
<div> 用戶評論:<%= comment %> </div>最終前端拿到的 HTML 是:
<div> 用戶評論:<script>alert('XSS!');</script> </div>惡意腳本將被執行。
客戶端渲染在使用innerHTML的時候也要十分注意。
反射型 XSS
- 插入點:HTML
- 攻擊步驟:
- 攻擊者構造一個帶有惡意代碼的 URL。
- 用戶打開該 URL 時,網站從 URL 中提取惡意代碼并插入 HTML。
- 用戶瀏覽器執行惡意代碼,導致數據泄露或偽造操作。
- 特點:反射型的特點是必須用戶點擊這個經過惡意構造的URL才會被攻擊,并且腳本顯現在URL上,相對來說比較容易被發現。反射型XSS有經過后端。
案例
一個搜索頁面,搜索的 query 通過 url 帶參。
php代碼:
<!-- search.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索結果</title> </head> <body> <h1>搜索結果</h1> <div> 你搜索了: <span id="search-query"><?php echo $_GET['q']; ?></span> </div> </body> </html>攻擊示例:
攻擊者可以構造一個攜帶惡意腳本的URL:
http://example.com/search.html?q=<script>alert('XSS Attack');</script>用戶點擊該鏈接后,服務器會直接將
q參數的值插入到頁面中,生成的頁面 HTML 如下:<div> 你搜素了: <span id="search-query"><script>alert('XSS Attack');</script></span> </div>瀏覽器解析并執行了
script標簽內的內容,從而觸發了 XSS 攻擊。
DOM 型 XSS
- 插入點:前端 JavaScript
- 攻擊步驟
- 攻擊者構造一個特殊的 URL。
- 用戶打開該 URL 后,前端 JavaScript 處理時會執行惡意代碼。
- 惡意代碼在瀏覽器中運行,可能導致數據泄露或偽造操作。
- 特點:DOM 型 XSS 僅發生在前端。
案例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>搜索結果</title> </head> <body> <h1>搜索結果</h1> <div> 正在搜索: <span id="search-query"></span> </div> <script> // 獲取 URL 參數中的 'q' 值 var query = new URLSearchParams(window.location.search).get('q'); // 直接將其插入到 DOM 中 document.getElementById('search-query').innerHTML = query; </script> </body> </html>攻擊者可以構造一個包含惡意代碼的鏈接:
http://example.com/search.html?q=<script>alert('DOM XSS Attack');</script>當鏈接被訪問時,惡意腳本就會被插入到界面中并執行:
document.getElementById('search-query').innerHTML = "<script>alert('DOM XSS Attack');</script>";
XSS的預防
XSS 有兩大要素,一是攻擊者提交惡意代碼,二是瀏覽器執行惡意代碼。
轉義用戶輸入(不實用)
為了預防攻擊者提交惡意代碼,可以考慮對用戶的輸入進行過濾。
使用合適的轉義函數,例如 escapeHTML(),將用戶輸入的特殊字符轉換為安全的 HTML 實體。
function escapeHTML(str) {
return str.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\//g, "/");
}
這種方法只能用于一些簡單的場景。它的缺點在于:
- 如果這個轉義僅發生在前端提交請求之前,攻擊者仍可以手動構造請求。
- 如果這個轉義發生在后端將數據寫入數據庫之前,也是不合理的,因為我們不知道這個數據將來應用的環境是哪里。上面這段代碼的轉義只能應用于瀏覽器環境的HTML里,轉義后的數據無法在桌面客戶端、安卓端等等其它環境上正常顯示。
總結:
- 轉義用戶輸入只能應用于簡單的場景;
- 轉義最好應用在Web前端渲染數據時,而不是用戶提交數據時;
- 對于明確的類型,比如數字、電話、URL、郵箱等數據,做一下數據過濾還是很有必要的。
用戶輸入環節并不能很好地預防 XSS,預防 XSS 的主要工作集中在預防瀏覽器執行惡意代碼上。
關于這一點又有兩類操作:
- 防止 HTML 被注入;
- 防止 JavaScript 執行惡意代碼。
預防反射型和存儲型
這兩種類型都是在后端服務器上完成模板拼接的過程中,將惡意代碼嵌入到了 HTML 中。
兩種解決方法分別是:客戶端渲染、轉義HTML。
純客戶端渲染
純客戶端渲染:客戶端訪問頁面,服務端返回的頁面不帶有任何業務數據,由前端的 JavaScript 通過 AJAX 異步請求數據后進行填充。
純客戶端渲染可以通過 .innerText,.setAttribute,.style等 API 來插入后端返回的業務內容(這些內容可能存在惡意代碼),通過這種方式插入的內容不會被瀏覽器當成代碼執行。
局限:
- 純客戶端渲染仍要注意 DOM型 XSS 攻擊。
- 部分頁面對于性能有較高需求,或者需要考慮SEO,仍需要服務端渲染。
轉義 HTML
對于服務端渲染,在拼接帶有業務數據的 HTML 時,要對 HTML 的各個插入點進行充分的轉義。
一些模板引擎可能僅對 & < > " ' / 進行轉義,這其實是遠遠不夠的,插入點有很多種,包括但不限于:
-
在 HTML 內嵌的文本中,以 script 標簽的形式注入;
-
在內聯的 javascript 中,拼接的數據突破了原本的限制(字符串、方法名等等);
例如,a 標簽的 href 可能理想狀態下是插入一個路徑鏈接,如果這個 href 插入的字符串是一個
javascript:惡意代碼的形式,當 a 標簽被點擊時,惡意代碼就會被執行。 -
對于標簽的屬性,如果注入的值包含雙引號,就會導致這個屬性的值提前結束,從而可以注入其它屬性或者標簽;
例如:
注入其它屬性示例
// src 是用用戶數據拼接的 <img src="<%= imgSrc %>" /> // 如果用戶數據如下 $imgSrc = './not-exist-img.png" onerror="javascript:惡意代碼' // 那么拼接后的 img 標簽就是 <img src="./not-exist-img.png" onerror="javascript:惡意代碼" />圖片加載不到,會觸發 onerror 中的惡意代碼。
注入其它標簽示例
<img src="<%= imgSrc %>" />如果 imgSrc 插入的內容是
" /> <script src="惡意代碼,那么最后就會變成:<img src="" /> <script src="惡意代碼" />
綜上,轉義 HTML 是一個很復雜的工作,通常在項目中會采用比較成熟的轉義庫。
預防DOM型
DOM型的XSS攻擊主要是前端的 Javascript 代碼不夠嚴謹,把不可信的用戶數據當作代碼執行了。
嵌入HTML
當需要把用戶輸入添加到頁面上時,盡量不要使用 innerHTML 和 document.write 這些API,而是使用 textContent。
同理,使用 Vue 或者 React 開發的時候,應該謹慎使用 v-html 和 dangerouslySetInnerHTML。
事件監聽
避免將不可信的數據拼接給 DOM 上的 location、onclick、onerror、onload、onmouseover等屬性。
動態執行JS
在 JS 中,可以通過執行 eval 函數、添加 script 標簽、Function構造函數等方法動態執行 JS 代碼。請確保動態執行的 JS 代碼是可信的。
動態執行 JS 是一種性能很差的做法,盡量不要使用。
其它預防措施
CSP
CSP,全稱 Content Security Policy。CSP 的主要目的是減少和防止 XSS 攻擊。通過嚴格控制可以在頁面上執行的腳本來源,CSP 可以有效阻止 XSS。
工作原理
通過 HTTP 頭 Content-Security-Policy 來定義的。這個頭部包含一系列指令,每個指令指定允許從哪些來源加載特定類型的資源。例如,script-src 指令可以用來限制從哪些域加載 JavaScript 腳本。
常見指令
default-src: 為其他未顯式指定的資源類型定義默認策略。script-src: 限制 JavaScript 資源的加載來源。style-src: 限制 CSS 樣式表的加載來源。img-src: 限制圖像的加載來源。connect-src: 限制 AJAX、WebSocket 等請求的目的地。font-src: 限制字體的加載來源。child-src:限制 Web Worker 腳本文件或者其它 iframe 等內嵌到文檔中的資源來源。
語法
不同的指令之間使用 ; 分隔,一條指令由指令和允許的若干個源構成。
常用的源包括:
'self': 允許資源從當前域加載(同源)。'none': 禁止加載任何資源。'unsafe-inline': 允許頁面內聯的資源(如<style>或<script>標簽中的內容),但存在安全風險,不推薦使用。'unsafe-eval': 允許使用eval()等動態代碼執行功能,也有安全風險,不推薦使用。- 具體域名: 如
https://example.com,允許資源僅從指定的域加載。 - 協議:如
http:、https:,尾部有冒號,但是不需要單引號。 - 通配符
\*: 允許資源從任何來源加載,不建議用于生產環境。
示例
Content-Security-Policy:
// 默認同源
default-src 'self';
// 只允許js從同源和具體的域名加載
script-src 'self' https://trusted-scripts.example.com;
// 允許css從同源加載,并允許內聯樣式(不建議)
style-src 'self' 'unsafe-inline';
// 只允許圖像從指定域名加載
img-src https://images.example.com;
// 只允許網絡請求(AJAX)連接到同源和指定的地址
connect-src 'self' https://api.example.com;
// 只允許字體從同源加載
font-src 'self';
// 禁止嵌入子文檔
frame-src 'none';
// 禁止加載任何插件資源
object-src 'none';
// 違反CSP的行為會被報告到指定的地址
report-uri /csp-report-endpoint;
除了使用 HTTP 頭部字段,也可以通過 meta 標簽配置:
// meta tag <meta http-equiv="Content-Security-Policy" content="default-src https:">
限制輸入內容長度
限制用戶輸入內容的長度可以提高構造XSS攻擊的難度。
HttpOnly Cookie
當 cookie 被設置為 Httponly 時,Cookie 只能通過 HTTP 請求頭(如 Set-Cookie 或 Cookie)進行傳輸,而不能被 JavaScript 通過 document.cookie 等方式讀取。
這種機制可以防止 cookie 信息被惡意腳本竊取。
示例HTTP頭
Set-Cookie:
sessionId=abc123;
HttpOnly;
Secure;
SameSite=Strict;
sessionId=abc123是 Cookie 的鍵值對;HttpOnly:表示這個 Cookie 只能通過 HTTP 請求傳輸,JavaScript 不能訪問;Secure:表示這個 Cookie 只能通過 HTTPS 傳輸;SameSite=Strict:限制 Cookie 僅在同站點請求時發送,防止 CSRF 攻擊。
XSS的檢測
在開發階段要預防XSS漏洞,對于已經上線的項目,檢測XSS漏洞的方法有:
-
使用通用 XSS 攻擊字符串手動檢測:
將下面這個字符串輸入到網站的各個輸入框,或者拼接到URL參數上,就可以進行檢測。只要
alert()被執行,就說明發現了XSS漏洞。(注意,前面的
jaVasCript:也要一起復制)jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk=alert() )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd=alert()//>\x3e -
使用一些自動掃描工具尋找XSS漏洞。
總結
- 責任:XSS防范需要前端后端共同參與;
- 轉義規則:需要根據業務場景選擇;
- 避免拼接HTML、避免使用內聯事件。
引用
[1] 前端安全系列(一):如何防止XSS攻擊? - 美團技術團隊 (meituan.com)

浙公網安備 33010602011771號