本文 AI 產出,尚未審核

JavaScript 課程 – 安全與防護

主題:安全的 JSON 解析


簡介

在前端與後端的資料交換過程中,JSON(JavaScript Object Notation) 是最常見的序列化格式。它不僅易於閱讀、寫作,也能直接在 JavaScript 中轉換為物件。然而,若在解析 JSON 時沒有適當的防護措施,攻擊者就可能利用惡意 payload 進行 XSS、原型汙染(prototype pollution)、或是 Denial‑of‑Service 等攻擊。

本篇文章將說明 為什麼安全的 JSON 解析很重要、介紹正確的解析方式與常見的陷阱,並提供可直接套用在專案中的程式碼範例,幫助初學者到中級開發者在實務上寫出更安全的程式。


核心概念

1. 為什麼不要直接使用 eval()

eval() 會將字串當成程式碼執行,若傳入的字串被惡意操控,攻擊者可以執行任意 JavaScript,造成 跨站腳本(XSS)。舊版教學常以 eval('(' + jsonStr + ')') 代替 JSON.parse(),這是 絕對不可取 的做法。

結論:永遠使用 JSON.parse(),除非你非常確定輸入是可信且已經過嚴格驗證。

2. JSON.parse() 本身的安全性

JSON.parse() 只會解析符合 JSON 語法的字串,並回傳純粹的 資料結構(object、array、number、string、boolean、null),不會執行任何程式碼。然而,仍有以下兩個需要注意的風險:

風險類型 說明 可能的危害
原型汙染 JSON 中若包含 __proto__constructor 等特殊屬性,解析後會修改全域物件原型 攻擊者可竄改所有物件的行為,造成任意屬性注入或執行惡意程式
資源耗盡 超大型或深度過深的 JSON 會使解析器耗盡記憶體或 CPU,導致 DoS 服務端或前端卡住、崩潰

3. 防止原型汙染的技巧

  1. 使用 reviver 函式JSON.parse(text, reviver) 允許在每個鍵值對被建立時進行過濾。
  2. 深層檢查:在解析完畢後,遍歷物件並剔除危險屬性。
  3. 套件協助:如 secure-json-parsejson-bigint(支援大數)等已內建防汙染機制。

4. 限制解析深度與大小

  • 前端:可在取得資料前先檢查 response.headers['content-length'] 或使用 AbortController 限制下載時間。
  • Node.js:使用 stream-json 以串流方式逐段解析,避免一次載入全部資料。

程式碼範例

以下示範 5 個實用範例,從最簡單的安全解析到進階的防汙染與資源限制。

範例 1:最基本的安全解析

// 假設收到的字串是從 API 回傳的 JSON
const jsonStr = '{"name":"Alice","age":30}';

try {
  const data = JSON.parse(jsonStr);   // ✅ 只會產生純資料
  console.log(data.name); // Alice
} catch (e) {
  console.error('JSON 解析失敗:', e);
}

重點JSON.parse 只接受符合 JSON 標準的字串,若字串不是合法 JSON,會拋出例外。


範例 2:使用 reviver 過濾危險屬性

const malicious = '{"__proto__":{"admin":true},"user":"bob"}';

function safeReviver(key, value) {
  // 直接過濾 __proto__、constructor、prototype 等屬性
  if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
    return undefined; // 讓該屬性被忽略
  }
  return value;
}

try {
  const obj = JSON.parse(malicious, safeReviver);
  console.log(obj); // { user: 'bob' }
  console.log({}.admin); // undefined,沒有被汙染
} catch (e) {
  console.error(e);
}

說明:reviver 在每一次鍵值對被建立時呼叫,返回 undefined 代表該鍵會被剔除。


範例 3:使用第三方套件 secure-json-parse

先安裝套件:

npm i secure-json-parse

然後在程式中使用:

import { parse } from 'secure-json-parse';

const payload = '{"constructor":{"prototype":{"evil":true}},"data":"safe"}';

try {
  const result = parse(payload); // 內部自動過濾危險屬性
  console.log(result); // { data: 'safe' }
} catch (e) {
  console.error('不安全的 JSON:', e);
}

優點:套件已經封裝好所有常見的汙染檢查,適合在大型專案中統一使用。


範例 4:限制 JSON 深度與大小(Node.js)

import { createReadStream } from 'fs';
import { parser } from 'stream-json';
import { streamValues } from 'stream-json/streamers/StreamValues';

const MAX_DEPTH = 10;   // 最大允許的巢狀層級
let currentDepth = 0;

const pipeline = createReadStream('large.json')
  .pipe(parser())
  .pipe(streamValues())
  .on('data', ({key, value}) => {
    // parser 會在每個結構開始時觸發 startObject/startArray
    if (key === 'startObject' || key === 'startArray') currentDepth++;
    if (key === 'endObject' || key === 'endArray') currentDepth--;

    if (currentDepth > MAX_DEPTH) {
      console.error('JSON 深度過深,已中止解析');
      pipeline.destroy(); // 立即停止串流
    }
  })
  .on('error', err => console.error('解析錯誤:', err));

說明:透過串流解析,可以即時偵測深度或大小,避免一次性載入巨量資料。


範例 5:前端使用 AbortController 防止惡意大檔

async function fetchSafeJSON(url, timeout = 3000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  try {
    const resp = await fetch(url, { signal: controller.signal });
    if (!resp.ok) throw new Error('Network response was not ok');

    // 先檢查 Content-Type,確保是 JSON
    const ct = resp.headers.get('content-type') || '';
    if (!ct.includes('application/json')) throw new Error('非 JSON 回應');

    const text = await resp.text();

    // 限制字串長度(例如 1 MB)
    const MAX_BYTES = 1 * 1024 * 1024;
    if (new TextEncoder().encode(text).length > MAX_BYTES) {
      throw new Error('回傳資料過大');
    }

    return JSON.parse(text); // 安全解析
  } catch (e) {
    console.error('取得或解析 JSON 失敗:', e);
    throw e;
  } finally {
    clearTimeout(id);
  }
}

重點:結合 AbortControllerContent‑Type 檢查字串長度限制,可以在前端有效防止過大或非 JSON 的回應。


常見陷阱與最佳實踐

陷阱 為何危險 正確做法
使用 eval()new Function() 解析 JSON 直接執行任意程式碼,易受 XSS 永遠改用 JSON.parse()
忽略 Content-Type 檢查 伺服器可能回傳 HTML、XML 或腳本 fetch 前先確認 application/json
直接信任外部 API 的回傳 API 可能被入侵或被中間人攻擊篡改 使用 HTTPS、驗證簽章(JWT / HMAC)
未處理 JSON.parse 的例外 解析失敗會拋出錯誤,若未捕獲會導致程式崩潰 使用 try / catch 包裹解析
原型汙染未被防範 __proto__constructor 會改變全域物件行為 使用 reviver、套件或自行過濾
解析過大或過深的 JSON 造成記憶體耗盡、CPU 飽和,導致 DoS 限制大小、深度、或改用串流解析

最佳實踐清單

  1. 永遠使用 JSON.parse(),絕不使用 eval
  2. 在解析前檢查 HTTP 標頭Content-Type 必須是 application/json
  3. 使用 try / catch 捕獲例外,避免未處理錯誤導致應用崩潰。
  4. 防止原型汙染:使用 reviver、白名單或已驗證的套件。
  5. 限制資料大小與深度:前端可用 AbortController,Node.js 可用 stream-json
  6. HTTPS + 簽章:確保傳輸過程不被竄改。
  7. 單元測試:寫測試案例驗證惡意 JSON 不會造成汙染或例外未捕獲。

實際應用場景

  1. SPA(單頁應用)與 API 串接
    前端在呼叫後端 RESTful API 時,必須保證回傳的 JSON 是安全的,否則惡意 payload 可能直接在瀏覽器執行,導致使用者資料外洩。

  2. Node.js 微服務間的訊息傳遞
    服務 A 以 JSON 形式傳遞指令給服務 B,若未過濾 __proto__,服務 B 的全域物件可能被汙染,進而影響後續所有請求。

  3. 第三方套件或插件的設定檔
    許多 npm 套件允許使用者提供 JSON 設定檔。若開發者直接 JSON.parse 而不防範原型汙染,攻擊者可在配置檔中植入惡意屬性,影響整個應用。

  4. IoT 裝置與雲端平台
    小型裝置常使用有限資源處理 JSON,若接收到過大的 payload,可能導致裝置資源枯竭,甚至無法回應正常指令。使用串流解析與大小限制是必要的防護措施。


總結

JSON 是現代 Web 應用不可或缺的資料交換格式,但 安全的解析 同樣重要。透過以下幾點,你就能大幅降低 JSON 相關的安全風險:

  1. 只使用 JSON.parse(),絕不使用 eval
  2. 檢查 Content-Type、限制大小與深度,避免 DoS。
  3. 防止原型汙染:使用 reviver、白名單或成熟套件。
  4. 捕獲例外與做好錯誤處理,確保程式不因解析失敗而崩潰。
  5. 在傳輸層使用 HTTPS、簽章,保護資料完整性。

掌握上述概念與範例後,你將能在 前端與後端 都寫出 安全、可靠 的 JSON 解析程式碼,為整體系統的防護奠定堅實基礎。祝你開發順利,寫出更安全的 JavaScript!