上回學習了 CSRF,但它的特徵是攻擊者「不知道使用者的認證資訊」「也無法讀取瀏覽器與伺服器之間傳輸的內容」。
攻擊者能做的,只是讓已登入的瀏覽器發送非本人意圖的請求。
當然,OWASP Top 10 中列出的攻擊手法非常多元,其中也有更直接地竄改資料,或是在使用者瀏覽器中執行的攻擊。
這次就來整理其中之一的 XSS,並鞏固基本的資安基礎知識。
在進入 XSS 的說明之前,先把兩者的差異釐清。
這是攻擊者先設下陷阱,等待使用者踩進去的攻擊。
前面學到的 CSRF 也屬於這一類。
例如,攻擊者在自己的網站上放置一段會向目標網站發送請求的程式碼,這就是一種陷阱。
<form action="https://example.com/password/change" method="POST">
<input type="hidden" name="newPassword" value="attacker123">
</form>
<script>document.forms[0].submit()</script>
使用者只要打開嵌入這段腳本的頁面,就會送出帶有 Cookie 的請求。
攻擊者在不知道認證資訊的情況下,也能讓使用者發出非本人意圖的請求。
XSS 也屬於這一類,後面會再說明。
這是攻擊者直接向伺服器送出請求的攻擊,當中不需要使用者操作介入。
也就是把精心設計過的值送進輸入欄位或 URL,讓伺服器把該值當作命令的一部分執行,藉此成立的攻擊。
SQL Injection 就屬於這一類。
雖然可以分成主動與被動,但兩者都可以用「資料與命令混在一起」來理解,會比較容易掌握。
XSS 是讓惡意腳本在其他使用者的瀏覽器中執行的攻擊。
近年資安攻擊中常見的釣魚信件,常常就是入口;攻擊者透過讓使用者點開陷阱 URL,或造訪被污染的頁面來達成攻擊。
攻擊流程
※跳脫(escape)是指把畫面中會有特殊意義的字元,例如 <、>、&、" 等,轉換成無害的字串。
例如若直接把 <script> 顯示在畫面上,腳本就可能直接執行。
讓 XSS 腳本執行的方法有很多種。
攻擊者建立含有腳本的 URL,透過電子郵件或 SNS 讓使用者點擊而成立。
例
https://example.com/search?q=<script>惡意程式碼</script>
在留言欄或個人檔案欄等地方,攻擊者輸入的內容會被儲存到資料庫中。
如果網站在顯示時沒有對內容進行跳脫,使用者一旦瀏覽,瀏覽器就會執行腳本,攻擊便成立。
例
<li>這篇文章不錯<script>alert('XSS')</script></li>
網站的 JavaScript 會從 URL 等來源讀取值,並以 HTML 的形式插入到畫面中,藉此成立。
這種型態有一個特別需要注意的特徵:因為不經過伺服器,而是在瀏覽器中完成,所以伺服器端的跳脫無法防範。
例
https://example.com/#<img src=x onerror=alert('XSS')>
※打開這個 URL 後,網站端程式會把 # 之後的值插入畫面,進而觸發 onerror。
反射型與儲存型是伺服器產生危險的 HTML 再回傳;DOM 型則是伺服器只回傳一般的 HTML。
DOM 型不會留下伺服器端的紀錄,而且連跳脫都無法防禦,因此我認為它是最棘手的。
防禦措施只有兩大方向:
消除成因,或是降低損害。
依序來看。
反射型與儲存型只要不要被渲染到畫面上即可,因此只要徹底進行跳脫轉換就能對應。
我第一次知道,像 React 這類框架在一般寫法下,會自動進行跳脫。
需要特別注意的是 DOM 型,JavaScript 將值放入畫面的流程中,必須
有意識地把資料與命令分開撰寫
例如把 innerHTML 改成 textContent 來塞進畫面。
// NG:把值當成 HTML 解析
element.innerHTML = name;
// OK:把值當成單純字串處理
element.textContent = name;
如果 name 是 <img src=x onerror=alert('XSS')>,innerHTML 會建立標籤並執行腳本。
textContent 則只會把這串文字原樣顯示在畫面上,不會執行。
雖然這裡以 innerHTML 為例,但像 eval 或 document.write 這類會把值當作程式碼解讀的處理,都需要特別小心。
同樣是用來消除成因的功能,這是瀏覽器原生提供的淨化功能。
使用 setHTML() 取代 innerHTML,就能移除可執行腳本的部分,只插入剩下安全的 HTML。
例如:
// NG:把值當成 HTML 解析
element.innerHTML = userHtml;
// OK:先移除危險部分再插入
element.setHTML(userHtml);
當你想保留粗體、圖片標籤等,也就是「希望 HTML 內容能保留表現力」時,可以使用它。
※截至 2026 年 10 月底,必須確認各瀏覽器的支援狀況。
Firefox:從 148(2026 年 2 月)開始可用
Chrome、Edge:web.dev 顯示從 146 起支援
Safari:未支援
若不支援,可以用 DOMPurify 這類函式庫替代。
即使是對資安不熟悉的我,也可能不知道上述防禦方式,或是忘記設定。
因此更需要透過多層防禦來降低損害,把攻擊者能做的事降到最低。
Cookie 加上 HttpOnly 之後,JavaScript 就無法讀取 Cookie。即使 XSS 成立,也不容易被竊取 session ID 等 Cookie。
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax
不過,腳本本身仍然會執行,因此畫面內容的讀取、或以使用者權限進行操作,仍然無法防止。
前面 CSRF 的對策,本質上是針對「從其他網站送來的請求」的防禦。
CSP(Content Security Policy,內容安全政策)是由伺服器指定允許執行的腳本來源的機制。
可以在安全性標頭中這樣寫:
Content-Security-Policy: script-src 'self'
這個設定下,只有從自己網站提供的腳本才能執行。直接寫在 HTML 裡的腳本,以及事件屬性(如 onerror)都會被瀏覽器阻止執行。即使有跳脫漏洞,攻擊腳本也有可能無法運作。
HttpOnly 的缺點是雖然能防止 Cookie 被竊取,卻無法阻止腳本本身的操作;而 CSP 的限制越嚴格就越穩固,但也會帶來每個外部服務整合都要額外設定,以及畫面較容易壞掉的取捨。
這些對策終究應該視為保險措施,
本體還是要靠跳脫與淨化來消除成因,這點很重要。
分類防禦策略角色注意事項成因輸出時跳脫反射型・儲存型中,把值轉成無害字串React 等框架在一般寫法下會自動進行成因安全的寫法(如 textContent)DOM 型中,把值當作單純字串放進畫面eval、document.write 等把值當作程式碼解讀的處理也需要注意成因Sanitizer API(setHTML())想保留 HTML 效果時,移除可執行腳本的部分再插入支援的瀏覽器有限,未支援時可用 DOMPurify 替代降低損害HttpOnly Cookie防止 Cookie 被竊取腳本仍會執行,因此無法防止畫面讀取或以使用者權限操作降低損害CSP阻止未被允許的腳本執行越嚴格越穩固,但會有外部服務整合設定成本與畫面容易壞掉的取捨重點是透過淨化、安全寫法與跳脫來消除成因!
HttpOnly 和 CSP 則作為無法完全排除時的多層防禦。
資安對策就從有名的項目開始,一點一點試著學習吧。
下一次想整理以 SQL Injection 為主的內容。
小林恭平・坂本陽 著、佐々木拓郎 監修《插圖圖解式 這一本就能搞懂 Web 技術的基礎》SB Creative
平野昌士 著,はせがわようすけ・後藤つぐみ 監修《前端開發的資安入門:不能再以不知道為藉口的漏洞對策必備知識》翔泳社
株式会社 xincere 正在招募沒有實務經驗的工程師與學生工程師實習生,並一起工作。
※ 關於シンシア的工作方式,請見這裡
在シンシア,每年約有 100 人左右沒有實務經驗的人申請並接受技術面試。
原文出處:https://qiita.com/Pigeon_gate/items/3ce4e1c1a39db64c5c32