在 JavaScript 中安全解析 JSON
JSON.parse 看似簡單卻有不少陷阱——原型污染、BigInt、不可信輸入。本文整理生產環境的解析檢查清單。
JSON.parse(text) 只有一行,這讓人誤以為 JSON 解析是個已解決的問題。
但只要輸入來自網路、檔案或使用者,它就不算。本文是 JavaScript 與
TypeScript 在生產環境安全解析 JSON 的實務檢查清單:JSON.parse 會
拋出什麼、如何在不撒謊的前提下加上型別,以及曾在真實程式碼造成漏洞
的原型污染陷阱。
JSON.parse 基本行為
JSON.parse(text, reviver?) 接受字串,回傳值;解析失敗時 拋出
SyntaxError——不會回傳 null 或 undefined。任何生產環境的呼叫
端都必須準備好接住。
const value = JSON.parse('{"a":1}'); // { a: 1 }
JSON.parse('{"a":'); // SyntaxError: Unexpected end of JSON input
拋出的錯誤包含 message、name,並會在訊息中附上解析器猜測的位置。
詳細解讀請參考 常見 JSON 語法錯誤。
永遠以 try/catch 包起來
不可信輸入意味著一定會出現解析失敗。任何一次未捕捉的拋出,足以讓 請求處理器崩潰、讓 React 樹掛掉。
function safeParse(text) {
try {
return { ok: true, value: JSON.parse(text) };
} catch (err) {
return { ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
失敗時應該記錄的兩件事:
- 錯誤訊息與位置。
- 輸入的 截短前綴(例如前 200 個字元)——絕不要記錄完整內文, 那可能含有機密,也可能是好幾 MB。
if (!result.ok) {
console.warn("JSON parse failed", {
error: result.error,
preview: text.slice(0, 200),
length: text.length,
});
}
reviver 函式
JSON.parse 第二個參數是 reviver——解析過程中每個 key/value 都會
呼叫一次,可用來轉換值。典型用法是把日期字串還原為 Date 物件:
const ISO_DATE = /^\d{4}-\d{2}-\d{2}T/;
function reviver(key, value) {
if (typeof value === "string" && ISO_DATE.test(value)) {
return new Date(value);
}
return value;
}
const data = JSON.parse('{"createdAt":"2026-05-13T10:00:00Z"}', reviver);
data.createdAt instanceof Date; // true
對於 BigInt 需要使用 sentinel:JSON 數字到了 reviver 時已經是雙精度
浮點數,精度損失發生在 reviver 之前。兩種解法:
- 大整數以字串序列化,在 reviver 中
BigInt()還原。 - 使用
json-bigint等在解析器更早介入的函式庫。
原型污染陷阱
這是多數開發者不知道的問題。JSON 規格允許 "__proto__" 作為物件鍵,
JSON.parse 會老實還原這個鍵——但若你接著用 不安全的 merge 把
解析結果合進另一個物件,某些 merge 實作會走 prototype chain,污染整
個 process 的 Object.prototype。
const evil = JSON.parse('{"__proto__":{"polluted":true}}');
// evil 本身沒事——但是…
const target = {};
naiveMerge(target, evil);
console.log({}.polluted); // true——所有物件都被加上這個屬性
防禦方式:
- 收容不可信鍵的 map,請用
Object.create(null)。 - 使用會拒絕
__proto__、constructor、prototype鍵的 merge 函式庫(Lodash 4.17.11+、defu或自己寫的)。 - 若只是讀取欄位、不做 merge,則安全。
同理:把解析結果當作 Map 的 key、或當作屬性名稱寫入
target[parsed.key] = value,攻擊者控制的 key 就是漏洞。
對不可信輸入做 schema 驗證
JSON.parse 證明的是輸入 語法 正確,不 證明輸入符合你預期的
形狀。安全模式是兩階段:先解析,再對 schema 驗證。
以 Zod 為例:
import { z } from "zod";
const User = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int().nonnegative(),
});
function parseUser(text: string) {
const json = JSON.parse(text); // 語法錯誤會拋出
return User.parse(json); // 形狀錯誤會拋出
}
對外暴露的伺服端 endpoint,建議在解析前先限制 輸入大小——一份
100 MB 全是 [[[[[… 的合法前綴會在出錯前先把記憶體耗光。超過預期
封包大小一律拒絕。
可使用 JSON 驗證工具 隨手以 schema 驗證 資料,也可用 JSON 轉 TypeScript 由 範本產生型別宣告。
串流與大型 payload
JSON.parse 是一次到位、整份載入記憶體的解析器。當資料量大到放不下:
- NDJSON——一行一筆 JSON。逐行讀取後
JSON.parse每行。雲端日誌都這樣輸出。 - 串流解析器——
clarinet、stream-json、或瀏覽器的TextDecoderStream配上自寫狀態機。適合單一巨大陣列、希望邊接收邊處理。
深入內容請參考 處理大型 JSON 檔案。
給結果加上型別(不撒謊)
JSON.parse 在 TypeScript 中回傳 any。誘惑是直接 cast:
const user = JSON.parse(text) as User; // 一個謊言——編譯器無從檢查
這在執行期會炸出難解的錯誤。誠實的做法是 解析成 unknown,再以
驗證器收斂:
const raw: unknown = JSON.parse(text);
const user = User.parse(raw); // zod 在形狀不符時拋出;結果有型別
如果你同時控制兩端、來源可信(自家後端、自家設定檔),cast 是務實的 選擇。除此之外,請驗證。
檢查清單
在把 JSON 解析交付生產前:
- 用 try/catch 包起來。
- 記錄錯誤與截短前綴,不要記錄完整內文。
- 對輸入加上大小限制。
- 對形狀做驗證(Zod、Yup、ajv、io-ts)——不要倚賴 cast。
- 在解析結果上避免會走
__proto__的 merge。 - 日期、BigInt 改用 reviver 或字串編碼。
- 資料量可能超過幾 MB 時改用串流解析器。
工具:
- JSON 驗證工具——語法加 schema 驗證。
- JSON 轉 TypeScript——由範本產出型別。
延伸閱讀
- 常見 JSON 語法錯誤——
JSON.parse會丟給你的錯誤類型。 - REST API 的 JSON 最佳實務——回到設計你要解析的 payload 本身。