flawopen.com/Prototype Pollution/Object.assign()
Object.assign()은 Prototype Pollution을 막나요?
완전히 그렇지는 않습니다. Object.assign()은 소스의 열거 가능한 고유 속성을 대상에 대입해 복사합니다. 소스에 열거 가능한 고유 __proto__ 키가 있으면 대입이 상속된 setter를 호출해 대상 객체의 프로토타입을 바꿀 수 있습니다. 얕은 대입 하나만으로 공유 Object.prototype이 바뀌지는 않지만 재귀 병합은 영향을 확대할 수 있습니다.
// A JSON own "__proto__" key reaches the inherited setter const source = JSON.parse('{"__proto__":{"isAdmin":true}}'); const target = Object.assign({}, source); // target's prototype changes; Object.prototype does not. // Recursive merge can reach Object.prototype function deepAssign(target, source) { for (const key of Object.keys(source)) { if (source[key] && typeof source[key] === 'object') { target[key] = target[key] || {}; deepAssign(target[key], source[key]); // With key === '__proto__', target[key] can be // Object.prototype; recursion then writes into it. } else { Object.assign(target, { [key]: source[key] }); } } }
// Spread creates an own data property; it does not invoke the setter. const shallow = { ...userInput }; // Reject dangerous keys at every depth and use own data properties. const blocked = new Set(['__proto__', 'constructor', 'prototype']); function isRecord(value) { if (value === null || typeof value !== 'object' || Array.isArray(value)) return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function copySafe(value) { if (Array.isArray(value)) return value.map(copySafe); return isRecord(value) ? mergeSafe(value) : value; } function mergeSafe(source, target = Object.create(null)) { if (!isRecord(source)) throw new TypeError('Expected a plain object'); for (const key of Object.keys(source)) { if (blocked.has(key)) continue; const value = source[key]; const current = Object.hasOwn(target, key) ? target[key] : undefined; const next = isRecord(value) ? mergeSafe(value, isRecord(current) ? current : Object.create(null)) : copySafe(value); Object.defineProperty(target, key, { value: next, enumerable: true, writable: true, configurable: true }); } return target; }
__proto__ 키에 대한 Object.assign() 동작
Object.assign()은 대상의 일반적인 속성 설정 동작으로 소스의 열거 가능한 고유 속성을 복사합니다. JSON에서 파싱한 소스에 고유 __proto__ 속성이 있고 대상이 기존 setter를 상속하면, 해당 setter가 대상의 [[Prototype]]을 바꿉니다. 이 경우 문자 그대로의 고유 __proto__ 속성이 만들어지지 않습니다.
객체 spread ({ ...source })는 결과에 데이터 속성을 생성하므로 같은 키가 고유 데이터 속성이 되고 상속 setter를 호출하지 않습니다. 얕은 복사에는 유용한 차이지만 이후 재귀 병합까지 막아주는 일반 방어는 아닙니다.
재귀 병합이 공유 프로토타입을 오염시키는 방식
안전하지 않은 재귀 코드가 공격자가 제어하는 키를 따라가 공유 프로토타입에 쓰면 Prototype Pollution이 발생합니다. 흔한 패턴은 중첩 객체를 찾으려고 target[key]를 읽습니다. key가 __proto__라면 Object.prototype을 반환할 수 있고, 다음 재귀 쓰기가 일반 객체 전반에 영향을 줄 수 있습니다. constructor.prototype 경로도 확인해야 합니다.
코드 리뷰에서는 공격자 제어 키로 대상을 읽거나 쓰는 작업과 setter 호출 여부를 찾고, 모든 재귀 단계에서 위험한 키를 거부하는지 확인하세요.
자주 묻는 질문
{ ...obj })도 똑같이 안전한가요?얕은 복사에서는 안전합니다. spread는 고유 데이터 속성을 만들어 상속 setter를 호출하지 않습니다. 반면 Object.assign()은 열거 가능한 __proto__ 소스 키를 받으면 대상 프로토타입을 바꿀 수 있습니다. 두 얕은 연산 모두 단독으로 공유 Object.prototype을 바꾸지는 않습니다.
structuredClone()은 별도 복사본을 만들며 소스의 프로토타입 체인을 적용하지 않습니다. 고유 __proto__ 데이터 키는 복사본에서도 데이터로 남지만 이후 안전하지 않은 병합에서 특별 취급될 수 있습니다.
유지관리되고 최신 상태인 병합 라이브러리를 사용하고 모든 중첩 단계에서 __proto__, constructor, prototype 처리 방식을 확인하세요. 널리 쓰이는 merge 및 object-path 도구 중 여러 개에서 Prototype Pollution 권고가 나왔으며 수정 버전에만 패치가 포함됩니다.