本文 AI 產出,尚未審核

JavaScript – 安全與防護(Security)

主題:Input Sanitization(輸入淨化)


簡介

在 Web 應用程式中,使用者的任何輸入都可能成為攻擊的入口。常見的跨站腳本(XSS)或 SQL 注入等漏洞,往往都是因為 未對輸入進行適當的淨化 而產生。即使前端已經使用 HTML 表單的 typepattern 等屬性限制格式,惡意使用者仍可繞過瀏覽器直接送出任意字串。

因此,在 JavaScript 中做好 Input Sanitization,不僅是保護伺服器端資料庫的第一道防線,也是提升使用者體驗、避免資料破壞的關鍵。本文將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立安全可靠的輸入淨化機制,適合剛入門的開發者,也能為中階開發者提供實務參考。


核心概念

1. 為什麼要「淨化」而不是「驗證」?

  • 驗證(Validation):檢查資料是否符合規則(例如 email 格式、長度範圍)。
  • 淨化(Sanitization):將資料中可能危險的字元或結構 轉義、移除或重新編碼,使其在後續處理時不會被解讀為指令。

簡單說:驗證是「這個值能不能接受」;淨化是「這個值進入系統後要變成什麼樣子才安全」。

2. 常見危險字元與攻擊向量

攻擊類型 常見危險字元或模式 可能的危害
XSS < > " ' / \ ` 注入惡意腳本,竊取 cookie、偽造 UI
SQLi ' " ; -- /* */ 竊取或破壞資料庫資料
Command Injection `; &&
HTML Injection <script>, </style> 改變頁面結構,釣魚或偽造訊息

3. 何時進行淨化?

  1. 使用者輸入:表單、URL 參數、Cookie、Header。
  2. 第三方 API 回傳資料:即使來源可信,也可能被中間人攻擊或格式錯誤。
  3. 儲存前或顯示前:根據需求決定是 在儲存前(防止資料庫污染)或 在輸出前(防止 XSS)進行淨化。

程式碼範例

下面提供 5 個實用的淨化範例,涵蓋文字、HTML、URL、SQL 以及 JSON,並加上詳細註解說明。

範例 1:基本的 HTML 文字轉義

/**
 * 將字串中的 HTML 特殊字元轉成實體,避免 XSS。
 * 只保留純文字,不允許任何 HTML 標籤。
 */
function escapeHTML(str) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#39;',
    '`': '&#96;'
  };
  return String(str).replace(/[&<>"'`]/g, m => map[m]);
}

// 使用範例
const userInput = '<script>alert("XSS")</script>';
const safeText = escapeHTML(userInput);
console.log(safeText); // &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

重點:此方法適合在 輸出到 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())後再淨化。
只在輸出前淨化 若資料已寫入資料庫或檔案,未淨化的內容仍可能被其他渠道讀取。 視情況決定:敏感資料建議在 寫入前 也做一次淨化。

最佳實踐清單

  1. 白名單優先:盡量列出允許的字元、標籤或屬性,避免黑名單的維護成本。
  2. 分層防護:前端、API、資料庫三層都要有驗證與淨化。
  3. 使用成熟函式庫:如 DOMPurifyxss-filtersvalidator.js,避免自行實作容易出錯。
  4. 正規化字串input = input.normalize('NFKC'); 可統一 Unicode 表示。
  5. 保持函式庫更新:安全函式庫會隨新漏洞快速更新,務必跟上版本。
  6. 測試與審計:使用自動化安全掃描(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 參數 使用 mysql2pg 等套件的 prepared statements,永不自行拼接字串。

案例說明:一個簡易的部落格平台,使用者可以發表文章。文章內容允許基本的排版(<b><i><a>),但必須防止 <script>onerror 等危險屬性。開發者在前端使用 DOMPurify 先淨化,再把結果送給後端,後端再次使用相同白名單做一次驗證,最後在資料庫儲存時使用參數化查詢。這樣的 多層防護 能極大降低 XSS 與 SQLi 的風險。


總結

  • Input Sanitization 是防止 XSS、SQLi、Command Injection 等攻擊的基礎工作,驗證與淨化必須同時執行
  • 透過 白名單成熟函式庫(DOMPurify、validator.js)以及 參數化查詢,可以在不同層級(前端、API、資料庫)建立可靠的安全防線。
  • 常見陷阱包括只靠前端驗證、手寫正則過濾、直接使用 innerHTML 等,遵循 分層防護、正規化、持續更新 的原則,可有效降低風險。
  • 在實務開發中,根據不同的 應用場景(表單、搜尋、API、留言板、資料庫寫入)選擇合適的淨化策略,才能兼顧 安全性使用者體驗

最後提醒:安全是一個持續的過程,定期檢視、測試與更新你的淨化機制,才能在不斷變化的威脅環境中保持防護力。祝你寫出乾淨、可靠的 JavaScript 程式碼!