FlawOpen

flawopen.com/취약점/JavaScript 프로토타입 오염

JavaScript 프로토타입 오염

심각도 높음 CWE-1321 공급망 및 런타임
쉽게 설명 — 공유 규칙책

새 장비가 자체 설정에서 답을 찾지 못하면 공용 규칙책을 참고하는 작업장을 떠올려 보세요. 관리자가 공격자가 정한 규칙을 적으면 다른 장비가 이를 기본값으로 읽을 수 있습니다. 프로토타입 오염에서는 안전하지 않은 동적 속성 쓰기가 객체의 프로토타입을 바꿀 수 있으며, 때로는 Object.prototype에 도달합니다. 영향이 자동으로 전역에 퍼지는 것은 아니며, 이후 코드가 상속된 속성을 신뢰해야 문제가 됩니다.

대상:신뢰할 수 없는 키를 병합하거나 사용자가 제어하는 속성 경로를 쓰는 서버 또는 브라우저 JavaScript
경로:__proto__ 같은 신뢰할 수 없는 키 또는 안전하지 않은 쓰기에 도달하는 constructor.prototype 경로
영향:도달 가능한 gadget에 따라 다릅니다. 로직 변경, 서비스 거부, XSS, 일부 서버 측 사례의 코드 실행이 가능하지만 자동으로 발생하지는 않습니다
대응:schema 또는 allowlist로 검증하고, 신뢰할 수 없는 키에는 Map 또는 프로토타입 없는 사전을 사용하세요

핵심 개념 및 하위 시스템 용어

Object.prototype
일반 객체가 상속하는 공유 프로토타입입니다. 오염되면 많은 객체에 영향을 줄 수 있지만 모든 JavaScript 객체나 realm에 영향을 주지는 않습니다.
__proto__
일반 객체의 레거시 접근자로 프로토타입을 가져오거나 설정합니다. JSON 텍스트에서 같은 표기는 이후 코드가 안전하지 않게 처리하기 전까지 일반 속성명입니다.
재귀적 깊은 병합
중첩 키를 순회해 다른 객체에 쓰는 복사 함수입니다. 신뢰할 수 없는 키가 프로토타입 관련 속성이나 안전하지 않은 경로를 선택하면 위험합니다.
CWE-1321
객체 프로토타입 속성에 대한 부적절하게 통제된 변경입니다. 취약점은 통제되지 않은 변경이며, 후속 영향은 도달 가능한 애플리케이션 동작에 달려 있습니다.

단계별 공격 흐름

단계 1

신뢰할 수 없는 속성 입력

엔드포인트가 공격자가 제어하는 객체 키를 받습니다. 고유 __proto__ 속성이 있는 JSON을 파싱하는 것만으로 프로토타입이 바뀌지는 않습니다.

단계 2

안전하지 않은 동적 쓰기

재귀 병합 또는 경로 setter가 이 키를 따라 각 경로 요소를 검증하지 않고 동적으로 씁니다.

단계 3

대상 또는 공유 프로토타입 변경

대상과 쓰기 순서에 따라 대상 프로토타입을 바꾸거나 Object.prototype 같은 공유 프로토타입에 도달합니다. 두 결과는 서로 다릅니다.

단계 4

영향은 gadget에 따라 달라짐

후속 gadget이 보안에 민감한 작업에서 상속된 값을 읽습니다. 결과는 해당 gadget에 달려 있으므로 인증 우회나 코드 실행을 단정하면 안 됩니다.

작동 원리와 근본 원인

JavaScript의 속성 조회는 프로토타입 체인을 따릅니다. 취약한 병합 또는 경로 setter는 __proto__나 constructor.prototype 같은 신뢰할 수 없는 키가 프로토타입에 쓰도록 유도할 수 있습니다. JSON.parse가 만든 고유 __proto__ 필드 자체는 오염이 아니며, 후속 코드가 안전하지 않은 할당으로 이를 해석해야 합니다. 대상 프로토타입 변경은 그 대상에 영향을 주고, Object.prototype 변경은 이를 상속하는 일반 객체에 영향을 줍니다. 보안 영향이 생기려면 별도의 gadget이 상속 속성을 사용해야 합니다.

utils.js (취약한 재귀 병합)취약
// 취약: 공격자가 제어하는 키가 재귀 쓰기에 도달함
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);
utils.js (allowlist 프로필 파서)보안 적용
// 보안 적용: 이 엔드포인트가 허용한 필드만 복사
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;
}

공격 및 악용 순서

방어 설계와 예방 규칙

자주 묻는 질문

JSON.parse()가 그 자체로 프로토타입을 오염시키나요?

아니요. JSON.parse()는 __proto__라는 자체 데이터 속성을 만들며 상속 setter를 호출하지 않습니다. 이후 merge나 대입이 setter를 호출하면 위험해집니다. JSON.parse()가 Prototype Pollution을 일으키나요?를 참조하세요.

Object.freeze(Object.prototype)이 해결책인가요?

동결된 Object.prototype 자체에 쓰는 것을 막을 수 있지만 setter가 개별 객체의 프로토타입을 바꾸거나 다른 모든 프로토타입을 보호하지는 않습니다. 내장 객체를 확장하는 라이브러리도 깨질 수 있습니다. 데이터 처리 수정이 아니라 defense-in-depth로만 사용하세요.

TypeScript는 안전한가요?

아니요. 타입은 runtime에 남지 않습니다. Record<string, string>으로 선언한 값에도 __proto__ 키가 있을 수 있습니다. 신뢰할 수 없는 키를 검증하고 객체를 안전하게 만드세요.

관련 보안 주제와 사고 분석 보기:전체 보안 디렉터리 →