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. 防止原型汙染的技巧
- 使用 reviver 函式:
JSON.parse(text, reviver)允許在每個鍵值對被建立時進行過濾。 - 深層檢查:在解析完畢後,遍歷物件並剔除危險屬性。
- 套件協助:如
secure-json-parse、json-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);
}
}
重點:結合 AbortController、Content‑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 | 限制大小、深度、或改用串流解析 |
最佳實踐清單:
- 永遠使用
JSON.parse(),絕不使用eval。 - 在解析前檢查 HTTP 標頭:
Content-Type必須是application/json。 - 使用
try / catch捕獲例外,避免未處理錯誤導致應用崩潰。 - 防止原型汙染:使用 reviver、白名單或已驗證的套件。
- 限制資料大小與深度:前端可用
AbortController,Node.js 可用stream-json。 - HTTPS + 簽章:確保傳輸過程不被竄改。
- 單元測試:寫測試案例驗證惡意 JSON 不會造成汙染或例外未捕獲。
實際應用場景
SPA(單頁應用)與 API 串接
前端在呼叫後端 RESTful API 時,必須保證回傳的 JSON 是安全的,否則惡意 payload 可能直接在瀏覽器執行,導致使用者資料外洩。Node.js 微服務間的訊息傳遞
服務 A 以 JSON 形式傳遞指令給服務 B,若未過濾__proto__,服務 B 的全域物件可能被汙染,進而影響後續所有請求。第三方套件或插件的設定檔
許多 npm 套件允許使用者提供 JSON 設定檔。若開發者直接JSON.parse而不防範原型汙染,攻擊者可在配置檔中植入惡意屬性,影響整個應用。IoT 裝置與雲端平台
小型裝置常使用有限資源處理 JSON,若接收到過大的 payload,可能導致裝置資源枯竭,甚至無法回應正常指令。使用串流解析與大小限制是必要的防護措施。
總結
JSON 是現代 Web 應用不可或缺的資料交換格式,但 安全的解析 同樣重要。透過以下幾點,你就能大幅降低 JSON 相關的安全風險:
- 只使用
JSON.parse(),絕不使用eval。 - 檢查
Content-Type、限制大小與深度,避免 DoS。 - 防止原型汙染:使用 reviver、白名單或成熟套件。
- 捕獲例外與做好錯誤處理,確保程式不因解析失敗而崩潰。
- 在傳輸層使用 HTTPS、簽章,保護資料完整性。
掌握上述概念與範例後,你將能在 前端與後端 都寫出 安全、可靠 的 JSON 解析程式碼,為整體系統的防護奠定堅實基礎。祝你開發順利,寫出更安全的 JavaScript!