__proto__ 주입이란
핵심 요약: JSON.parse는 __proto__를 평범한 키로 처리하고, 손상은 Object.prototype에 쓰는 재귀 병합에서 일어나 모든 객체가 공격자 속성을 상속합니다.

핵심부터 말하면, 병합 전에 __proto__, constructor, prototype을 차단하고, 신뢰할 수 없는 입력은 Object.create(null)에 병합하며, CI에 프로토타입 단언을 더하세요. 라이브러리 패치만으로는 부족합니다.
한 문장으로: __proto__라는 신뢰할 수 없는 키가 재귀 병합을 만나고, 병합이 프로토타입에 씁니다.
재현
function merge(target, source) {
for (const key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
target[key] = target[key] || {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
merge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log({}.isAdmin); // true
업무 객체는 아무것도 건드리지 않았습니다. Object.prototype이 변했고 모든 객체가 이제 isAdmin을 지닙니다.
JSON.parse가 잘못이 아닌 이유
const data = JSON.parse('{"__proto__": {"a": 1}}');
Object.getOwnPropertyNames(data); // [ '__proto__' ]
Object.getPrototypeOf(data) === Object.prototype; // true, untouched
__proto__는 평범한 자체 속성으로 도착합니다. 위험은 이후 병합, 딥 복사, 경로 설정 코드에 전적으로 있습니다. _.merge, 일부 Object.assign 패턴, 수많은 손수 만든 set(obj, path, value) 헬퍼입니다.
영향
- 권한 우회:
if (user.isAdmin)이 무조건 참이 됨 - 데이터 오염: 예상 밖 필드가 저장 레코드에 직렬화됨
- 서비스 거부: 프로토타입에 붙은 큰 객체가 메모리를 영구히 키움
- 에스컬레이션: 템플릿 엔진이나 명령 실행기와 결합되어 원격 코드 실행에 이른 사례가 있음
네 가지 방어
키를 필터하세요.
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
__proto__만 차단하면 불충분합니다. constructor.prototype이 같은 곳에 도달합니다.
null 프로토타입 객체로 병합하세요.
const safeTarget = Object.create(null);
프로토타입 체인이 없으면 __proto__는 그냥 문자열 키입니다.
in 대신 Object.hasOwn을 쓰세요. in은 프로토타입 체인을 조회합니다.
의존성을 패치한 상태로 유지하세요. lodash, deepmerge, json-patch 모두 이 부류 버그의 수정을 출시했습니다. npm audit을 주시하세요.
고정됨을 검증하기
__proto__와 constructor가 든 페이로드를 병합에 통과시킨 뒤 단언하세요:
console.log({}.polluted); // undefined
console.log(Object.prototype.polluted); // undefined
CI의 단언 하나, 비용 거의 제로로 이후 리팩터링의 회귀를 잡습니다.
FAQ
병합이 필터해야 할 키는?
__proto__, constructor, prototype을 함께 차단하세요. __proto__만 필터하면 불충분합니다. constructor.prototype이 같은 프로토타입에 도달합니다.
Object.create(null)이 왜 쓰기를 막나?
null 프로토타입 객체는 프로토타입 체인이 없어 __proto__가 평범한 문자열 키로 남습니다. 데이터 컨테이너로 동작하지만 키 필터링이나 의존성 업데이트를 대체하지는 않습니다.
CI에서 프로토타입 오염을 어떻게 테스트하나?
__proto__와 constructor가 든 페이로드를 병합에 통과시킨 뒤 {}.polluted와 Object.prototype.polluted가 undefined인지 단언하세요. 단언 하나는 비용이 거의 없고 이후 리팩터링의 회귀를 잡습니다.