JavaScript – 安全與防護(Security)
主題:Input Sanitization(輸入淨化)
簡介
在 Web 應用程式中,使用者的任何輸入都可能成為攻擊的入口。常見的跨站腳本(XSS)或 SQL 注入等漏洞,往往都是因為 未對輸入進行適當的淨化 而產生。即使前端已經使用 HTML 表單的 type、pattern 等屬性限制格式,惡意使用者仍可繞過瀏覽器直接送出任意字串。
因此,在 JavaScript 中做好 Input Sanitization,不僅是保護伺服器端資料庫的第一道防線,也是提升使用者體驗、避免資料破壞的關鍵。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立安全可靠的輸入淨化機制,適合剛入門的開發者,也能為中階開發者提供實務參考。
核心概念
1. 為什麼要「淨化」而不是「驗證」?
- 驗證(Validation):檢查資料是否符合規則(例如 email 格式、長度範圍)。
- 淨化(Sanitization):將資料中可能危險的字元或結構 轉義、移除或重新編碼,使其在後續處理時不會被解讀為指令。
簡單說:驗證是「這個值能不能接受」;淨化是「這個值進入系統後要變成什麼樣子才安全」。
2. 常見危險字元與攻擊向量
| 攻擊類型 | 常見危險字元或模式 | 可能的危害 |
|---|---|---|
| XSS | < > " ' / \ ` |
注入惡意腳本,竊取 cookie、偽造 UI |
| SQLi | ' " ; -- /* */ |
竊取或破壞資料庫資料 |
| Command Injection | `; | && |
| HTML Injection | <script>, </style> |
改變頁面結構,釣魚或偽造訊息 |
3. 何時進行淨化?
- 使用者輸入:表單、URL 參數、Cookie、Header。
- 第三方 API 回傳資料:即使來源可信,也可能被中間人攻擊或格式錯誤。
- 儲存前或顯示前:根據需求決定是 在儲存前(防止資料庫污染)或 在輸出前(防止 XSS)進行淨化。
程式碼範例
下面提供 5 個實用的淨化範例,涵蓋文字、HTML、URL、SQL 以及 JSON,並加上詳細註解說明。
範例 1:基本的 HTML 文字轉義
/**
* 將字串中的 HTML 特殊字元轉成實體,避免 XSS。
* 只保留純文字,不允許任何 HTML 標籤。
*/
function escapeHTML(str) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'`': '`'
};
return String(str).replace(/[&<>"'`]/g, m => map[m]);
}
// 使用範例
const userInput = '<script>alert("XSS")</script>';
const safeText = escapeHTML(userInput);
console.log(safeText); // <script>alert("XSS")</script>
重點:此方法適合在 輸出到 HTML 前使用,保證瀏覽器不會把字串當成程式碼執行。
範例 2:允許特定 HTML 標籤的淨化(使用 DOMPurify)
// 先安裝: npm i dompurify
import DOMPurify from 'dompurify';
/**
* 只允許 <b>, <i>, <u>, <a> 四種標籤,其他全部移除。
*/
function sanitizeHTML(html) {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'u', 'a'],
ALLOWED_ATTR: ['href', 'title', 'target']
});
}
// 範例
const dirty = '<p onclick="steal()">Hello <b>World</b> <script>alert(1)</script></p>';
const clean = sanitizeHTML(dirty);
console.log(clean); // Hello <b>World</b>
說明:
DOMPurify是業界常用的淨化庫,能自動處理各種 XSS 變形,且效能佳。若只需要 白名單(允許的標籤)方式,這是最安全且最省力的選擇。
範例 3:URL 參數的安全編碼
/**
* 對 URL Query String 中的值作 encode,防止注入惡意參數。
*/
function buildSafeURL(base, params) {
const query = Object.entries(params)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
return `${base}?${query}`;
}
// 使用
const url = buildSafeURL('https://example.com/search', {
q: '<script>alert(1)</script>',
page: 2
});
console.log(url);
// https://example.com/search?q=%3Cscript%3Ealert%281%29%3C%2Fscript%3E&page=2
技巧:
encodeURIComponent會把所有危險字元轉成%XX,即使使用者輸入<script>也只能當作文字傳遞。
範例 4:防止 SQL 注入的參數化查詢(以 Node.js + mysql2 為例)
// npm i mysql2
import mysql from 'mysql2/promise';
/**
* 使用 prepared statement,將使用者輸入作為參數傳遞,避免手動字串拼接。
*/
async function getUserByEmail(email) {
const connection = await mysql.createConnection({host: 'localhost', user: 'root', database: 'demo'});
const [rows] = await connection.execute(
'SELECT id, name FROM users WHERE email = ?', // ? 位置會自動安全轉義
[email] // 參數陣列
);
await connection.end();
return rows;
}
// 測試
getUserByEmail("test@example.com' OR '1'='1")
.then(console.log)
.catch(console.error);
關鍵:永遠使用參數化查詢(prepared statements)或 ORM 提供的安全 API,千萬不要自行拼接字串。
範例 5:JSON 輸入的深層淨化
/**
* 遞迴遍歷物件,對所有字串屬性做 HTML 轉義。
* 這在把使用者提供的 JSON 渲染到前端時非常有用。
*/
function sanitizeObject(obj) {
if (Array.isArray(obj)) {
return obj.map(sanitizeObject);
} else if (obj && typeof obj === 'object') {
const clean = {};
for (const [k, v] of Object.entries(obj)) {
clean[k] = sanitizeObject(v);
}
return clean;
} else if (typeof obj === 'string') {
return escapeHTML(obj); // 使用前面的 escapeHTML 函式
}
return obj; // 其他類型直接回傳
}
// 範例
const dirtyJSON = {
title: '<b>Bad</b>',
comments: [
{ user: 'alice', text: '<script>alert(1)</script>' },
{ user: 'bob', text: 'Nice! 😊' }
]
};
const safeJSON = sanitizeObject(dirtyJSON);
console.log(JSON.stringify(safeJSON, null, 2));
應用:當後端回傳 JSON 給前端,若直接 innerHTML 內嵌,仍可能產生 XSS。先對 JSON 做深層淨化,可降低此風險。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| 只在前端驗證 | 前端驗證可以被繞過,若只靠它就會留下漏洞。 | 在伺服器端再次驗證與淨化,前端僅作為使用者體驗的輔助。 |
| 自行寫正則過濾 | 正則很難覆蓋所有 XSS 變形,容易漏掉新型攻擊。 | 使用成熟的淨化函式庫(如 DOMPurify、sanitize-html)。 |
直接使用 innerHTML |
若未經淨化直接插入字串,即使字串看起來安全也可能被利用。 | 永遠使用 textContent 或已淨化的 HTML。 |
| 忽略 Unicode 同形字 | 攻擊者會利用全形、半形、混合編碼(例如 lesson)繞過過濾。 |
采用 Unicode 正規化(String.prototype.normalize())後再淨化。 |
| 只在輸出前淨化 | 若資料已寫入資料庫或檔案,未淨化的內容仍可能被其他渠道讀取。 | 視情況決定:敏感資料建議在 寫入前 也做一次淨化。 |
最佳實踐清單
- 白名單優先:盡量列出允許的字元、標籤或屬性,避免黑名單的維護成本。
- 分層防護:前端、API、資料庫三層都要有驗證與淨化。
- 使用成熟函式庫:如
DOMPurify、xss-filters、validator.js,避免自行實作容易出錯。 - 正規化字串:
input = input.normalize('NFKC');可統一 Unicode 表示。 - 保持函式庫更新:安全函式庫會隨新漏洞快速更新,務必跟上版本。
- 測試與審計:使用自動化安全掃描(OWASP ZAP、Burp Suite)或單元測試檢驗淨化效果。
實際應用場景
| 場景 | 需要的淨化類型 | 典型實作 |
|---|---|---|
| 使用者留言板 | HTML 文字、圖片 URL | DOMPurify 白名單 <p><a><img>,同時 encodeURI 處理圖片來源。 |
| 搜尋欄位 | URL 參數 | encodeURIComponent 編碼每個關鍵字,防止搜尋語法被注入。 |
| 會員註冊 | Email、密碼、姓名 | validator.isEmail() 驗證,escapeHTML 處理姓名顯示。 |
| REST API 回傳 JSON | JSON 內嵌 HTML | sanitizeObject 深層淨化,或在前端使用 textContent 渲染。 |
| 伺服器端資料庫寫入 | SQL 參數 | 使用 mysql2、pg 等套件的 prepared statements,永不自行拼接字串。 |
案例說明:一個簡易的部落格平台,使用者可以發表文章。文章內容允許基本的排版(
<b>、<i>、<a>),但必須防止<script>、onerror等危險屬性。開發者在前端使用DOMPurify先淨化,再把結果送給後端,後端再次使用相同白名單做一次驗證,最後在資料庫儲存時使用參數化查詢。這樣的 多層防護 能極大降低 XSS 與 SQLi 的風險。
總結
- Input Sanitization 是防止 XSS、SQLi、Command Injection 等攻擊的基礎工作,驗證與淨化必須同時執行。
- 透過 白名單、成熟函式庫(DOMPurify、validator.js)以及 參數化查詢,可以在不同層級(前端、API、資料庫)建立可靠的安全防線。
- 常見陷阱包括只靠前端驗證、手寫正則過濾、直接使用
innerHTML等,遵循 分層防護、正規化、持續更新 的原則,可有效降低風險。 - 在實務開發中,根據不同的 應用場景(表單、搜尋、API、留言板、資料庫寫入)選擇合適的淨化策略,才能兼顧 安全性 與 使用者體驗。
最後提醒:安全是一個持續的過程,定期檢視、測試與更新你的淨化機制,才能在不斷變化的威脅環境中保持防護力。祝你寫出乾淨、可靠的 JavaScript 程式碼!