flawopen.com/취약점/JavaScript 프로토타입 오염
JavaScript 프로토타입 오염
새 장비가 자체 설정에서 답을 찾지 못하면 공용 규칙책을 참고하는 작업장을 떠올려 보세요. 관리자가 공격자가 정한 규칙을 적으면 다른 장비가 이를 기본값으로 읽을 수 있습니다. 프로토타입 오염에서는 안전하지 않은 동적 속성 쓰기가 객체의 프로토타입을 바꿀 수 있으며, 때로는 Object.prototype에 도달합니다. 영향이 자동으로 전역에 퍼지는 것은 아니며, 이후 코드가 상속된 속성을 신뢰해야 문제가 됩니다.
__proto__ 같은 신뢰할 수 없는 키 또는 안전하지 않은 쓰기에 도달하는 constructor.prototype 경로Map 또는 프로토타입 없는 사전을 사용하세요핵심 개념 및 하위 시스템 용어
- Object.prototype
- 일반 객체가 상속하는 공유 프로토타입입니다. 오염되면 많은 객체에 영향을 줄 수 있지만 모든 JavaScript 객체나 realm에 영향을 주지는 않습니다.
- __proto__
- 일반 객체의 레거시 접근자로 프로토타입을 가져오거나 설정합니다. JSON 텍스트에서 같은 표기는 이후 코드가 안전하지 않게 처리하기 전까지 일반 속성명입니다.
- 재귀적 깊은 병합
- 중첩 키를 순회해 다른 객체에 쓰는 복사 함수입니다. 신뢰할 수 없는 키가 프로토타입 관련 속성이나 안전하지 않은 경로를 선택하면 위험합니다.
- CWE-1321
- 객체 프로토타입 속성에 대한 부적절하게 통제된 변경입니다. 취약점은 통제되지 않은 변경이며, 후속 영향은 도달 가능한 애플리케이션 동작에 달려 있습니다.
단계별 공격 흐름
신뢰할 수 없는 속성 입력
엔드포인트가 공격자가 제어하는 객체 키를 받습니다. 고유 __proto__ 속성이 있는 JSON을 파싱하는 것만으로 프로토타입이 바뀌지는 않습니다.
안전하지 않은 동적 쓰기
재귀 병합 또는 경로 setter가 이 키를 따라 각 경로 요소를 검증하지 않고 동적으로 씁니다.
대상 또는 공유 프로토타입 변경
대상과 쓰기 순서에 따라 대상 프로토타입을 바꾸거나 Object.prototype 같은 공유 프로토타입에 도달합니다. 두 결과는 서로 다릅니다.
영향은 gadget에 따라 달라짐
후속 gadget이 보안에 민감한 작업에서 상속된 값을 읽습니다. 결과는 해당 gadget에 달려 있으므로 인증 우회나 코드 실행을 단정하면 안 됩니다.
작동 원리와 근본 원인
JavaScript의 속성 조회는 프로토타입 체인을 따릅니다. 취약한 병합 또는 경로 setter는 __proto__나 constructor.prototype 같은 신뢰할 수 없는 키가 프로토타입에 쓰도록 유도할 수 있습니다. JSON.parse가 만든 고유 __proto__ 필드 자체는 오염이 아니며, 후속 코드가 안전하지 않은 할당으로 이를 해석해야 합니다. 대상 프로토타입 변경은 그 대상에 영향을 주고, Object.prototype 변경은 이를 상속하는 일반 객체에 영향을 줍니다. 보안 영향이 생기려면 별도의 gadget이 상속 속성을 사용해야 합니다.
// 취약: 공격자가 제어하는 키가 재귀 쓰기에 도달함
function merge(target, source) {
for (const key in source) {
const value = source[key];
if (value && typeof value === 'object') {
if (!target[key]) target[key] = {};
merge(target[key], value);
} else {
target[key] = value;
}
}
return target;
}
// JSON.parse는 고유 __proto__ 데이터 속성을 만든다. 위험은 안전하지 않은 병합이다.
const input = JSON.parse('{"__proto__": {"isAdmin": true}}');
merge({}, input);
// 보안 적용: 이 엔드포인트가 허용한 필드만 복사
const PROFILE_KEYS = new Set(['displayName', 'theme']);
function parseProfile(input) {
if (!input || typeof input !== 'object' || Array.isArray(input)) {
throw new TypeError('profile must be an object');
}
const profile = Object.create(null);
for (const key of Object.keys(input)) {
if (!PROFILE_KEYS.has(key)) continue;
const value = input[key];
if (typeof value !== 'string' || value.length > 80) {
throw new TypeError('invalid profile field');
}
profile[key] = value;
}
return profile;
}
공격 및 악용 순서
- 1입력: 요청에
{"__proto__": {"admin": true}}같은 예상치 못한 객체 키가 포함됩니다. JSON 파싱만으로 프로토타입이 바뀌지는 않습니다. - 2안전하지 않은 쓰기: 일반 병합 또는 경로 setter가 allowlist 없이 해당 키를 처리합니다.
- 3프로토타입 도달: 코드 경로에 따라 쓰기가 대상 프로토타입을 바꾸거나 공유 프로토타입에 도달할 수 있습니다.
- 4영향 gadget: 후속 코드가 민감한 판단에서 상속 속성을 신뢰해야 오염이 영향을 줍니다. 실제 gadget과 권한 확인 경로를 테스트하세요.
방어 설계와 예방 규칙
- ✓schema allowlist를 우선하세요: 엔드포인트가 허용한 필드만으로 대상을 만드세요. 일반 경로 작성이 불가피하면 모든 깊이에서 프로토타입 관련 경로 요소를 거부하세요.
- ✓안전한 사전을 사용하세요: 임의 사용자 키는
Map또는Object.create(null)사전에 저장하고, 일반 객체를 기대하는 API에 넘기기 전에 검증하세요. - ✓고유 속성을 확인하세요: 보안에 민감한 존재 여부 확인에는
Object.hasOwn(record, key)를 사용하고 상속된 기본값으로 작업을 승인하지 마세요. - ✓런타임 강화는 심층 방어로 사용하세요: Node.js
--disable-proto=throw는 한 경로를 제거하지만constructor.prototype은 막지 않습니다. 내장 객체 동결은 polyfill과 의존성을 깨뜨릴 수 있으므로 호환성을 확인하고 안전한 쓰기의 대체 수단으로 취급하지 마세요.
자주 묻는 질문
아니요. JSON.parse()는 __proto__라는 자체 데이터 속성을 만들며 상속 setter를 호출하지 않습니다. 이후 merge나 대입이 setter를 호출하면 위험해집니다. JSON.parse()가 Prototype Pollution을 일으키나요?를 참조하세요.
동결된 Object.prototype 자체에 쓰는 것을 막을 수 있지만 setter가 개별 객체의 프로토타입을 바꾸거나 다른 모든 프로토타입을 보호하지는 않습니다. 내장 객체를 확장하는 라이브러리도 깨질 수 있습니다. 데이터 처리 수정이 아니라 defense-in-depth로만 사용하세요.
아니요. 타입은 runtime에 남지 않습니다. Record<string, string>으로 선언한 값에도 __proto__ 키가 있을 수 있습니다. 신뢰할 수 없는 키를 검증하고 객체를 안전하게 만드세요.
- MDN — JavaScript 프로토타입 오염
- OWASP — 프로토타입 오염 방지 가이드
- CWE-1321 — 객체 프로토타입 속성에 대한 부적절하게 통제된 변경
- MDN — Object.prototype.__proto__ 접근자