JS prototype 개념
prototype pollution을 이해하기 위해서는 JS의 object prototype과 constructor에 대한 개념을 알아야한다.
자바스크립트는 다음과 같은 특징이 있다.
- 모든 것은 객체이고, 객체로서 동작하는 완전한 객체지향 언어다.
var keyme = new Person('keyme');
- 객체는 생성자의 prototype 객체를 상속한다.
- keyme라는 객체는 Person.prototype을 상속받는다.
- 객체는 생성자 함수를 사용해서 만든다.
- 생성자 함수 → Person()
prototype은 객체 지향 프로그래밍에서 상속을 구현하는 데 사용되는 중요한 개념이다. 모든 객체는 prototype을 가지고 있으며, 이를 통해 객체 간의 상속 및 속성, 메서드 공유가 가능하다.
function Person(name){
this.name = name;
}
//생성자 함수를 이용해 객체를 생성한다. new 키워드를 이용한다.
var keyme = new Person('keyme');
var user = new Person('user');
// 2개의 객체를 생성했다. 2개의 객체에 동시에 메서드를 상속하고 싶다면,
// 이때, prototype 객체를 이용할 수 있다. Person 생성자함수가 prototype 객체를 참조하도록 만들 수 있다.
Person.prototype.tellName = function() {
console.log(this.name);
};
// 생성자 함수를 이용해 생성한 두 객체는 prototype객체를 참조하기 때문에 tellName 메서드를 이용할 수 있다.
//이 특성을 이용해 자바스크립트는 prototype을 이용해 상속을 구현한다.
keyme.tellName();
user.tellName();
// 상속된 prototype 객체는 _proto_ 프로퍼티에 담긴다.
console.log(Person.prototype === keyme._proto_); //true
// 객체의 constructor 프로퍼티는 자신을 생성한 객체를 가리킨다.
// Person의 prototype을 생성한 객체는 Person이다.
console.log(Person.prototype.constructor === Person); //true
위 코드는 prototype과 construct의 개념을 쉽게 이해할 수 있는 코드다.
본래 prototype은 메서드나 변수를 여러 객체에 동시에 상속하고 싶은 경우에 사용하는 기능이다.
//생성자 함수를 이용해 객체를 생성한다. new 키워드를 이용한다.
var keyme = new Person('keyme');
var user = new Person('user');
// 2개의 객체를 생성했다. 2개의 객체에 동시에 메서드를 상속하고 싶다면,
// 이때, prototype 객체를 이용할 수 있다. Person 생성자함수가 prototype 객체를 참조하도록 만들 수 있다.
Person.prototype.tellName = function() {
console.log(this.name;
};
// 생성자 함수를 이용해 생성한 두 객체는 prototype객체를 참조하기 때문에 tellName 메서드를 이용할 수 있다.
//이 특성을 이용해 자바스크립트는 prototype을 이용해 상속을 구현한다.
keyme.tellName();
user.tellName();
위 코드는 prototype을 이용해서 Person 생성자의 prototype에 함수를 정의한 코드다.
prototype에 함수를 정의했으므로 Person 생성자를 상속받은 keyme, user 객체는 메서드를 상속받는다.
이것이 가능한 이유는 keyme, user 객체가 생성되는 과정에서 Person.prototype을 상속 받았기 때문이다.
console.log(Person.prototype === keyme._proto_); //true
위 코드를 보자.
keyme 객체의 __proto__
가 Person 생성자의 prototype과 동일한 것을 확인할 수 있다.
이처럼 __proto__
는 상위의 prototype 객체를 가리킨다.
keyme.__proto__.__proto__
먄약, 이런 식으로 쭉 거슬러 올라가면 최상위 객체인 Object의 prototype을 가리키게 된다.
즉, 모든 객체는 Object.prototype 객체를 상속받는다는 것을 알고 넘어가자.
Prototype Pollution
Prototype Pollution은 JS에서 객체를 처리하는 로직에 문제가 있는 경우에 발생할 수 있는 취약점이다.
Prototype Pollution이 발생하면, Object의 prototype을 수정할 수 있다.
Object의 prototype을 변경할 수 있는 경우에는 개발자가 의도한 로직을 우회하거나 DOM에 관여하여 XSS 등의 2차적인 취약점으로 연계할 수 있다.
Object.__proto__
Object.constructor.prototype
prototype pollution은 위 2가지 키워드를 이용해서 공격을 수행할 수 있다.
이전에 최상위 객체는 Object 객체라고 했다.
→ 즉, 모든 객체는 Object.prototype 객체를 상속받는다.
만약, object 객체에 정의된 tostring()
를 prototype pollution으로 재정의하면 어떻게 될까?
var test1 = 1; // int (Number)
var test2 = 2; // int (Number)
console.log(test1.constructor); // function Number()
console.log(test2.constructor); // function Number()
console.log(test2.toString()); // "2"
test1.constructor.prototype.toString = function(){return "hacked"}
console.log(test2.toString()); // "hacked"
위 코드는 prototype pollution 취약점을 이용해서 최상위인 Object 객체의 prototype에 정의된 tostring()
를 변조하는 과정이다.
실행 결과를 보면, 변조된 함수가 실행된 것을 확인할 수 있다.
실제 prototype pollution은 JS에서 객체를 잘못 처리하는 경우 발생한다.
어떤 경우에 발생하는지 알아보자.
○ SetProperty
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function setValue(obj, key, value) {
const keylist = key.split('.');
const e = keylist.shift();
if (keylist.length > 0) {
if (!isObject(obj[e])) obj[e] = {};
setValue(obj[e], keylist.join('.'), value);
} else {
obj[key] = value;
return obj;
}
}
const obj1 = {};
setValue(obj1, "__proto__.hacked", 45);
const obj2 = {};
obj2.hacked; // 1
사용자가 key를 입력할 수 있는 경우에 발생할 수 있다.
setValue()
를 보자.
2번째 인자인 key를 “.” 기준으로 split 했을 때의 결과가 1개 이상인 경우에는 setValue()
를 재귀적으로 실행한다.
재귀적으로 실행되는 setValue()
는 다음과 같이 실행된다.
setValue(obj1['__proto__'], "hacked", 45);
결국, obj1의 __proto__
에 hacked = 45를 세팅하는 코드가 실행된다.
이런 식으로 key를 입력해서 property를 설정하는 로직이 있는 경우에는 prototype pollution이 가능하다.
○ merge
function merge(a, b) {
for (let key in b) {
if (isObject(a[key]) && isObject(b[key])) {
merge(a[key], b[key]);
} else {
a[key] = b[key];
}
}
return a;
}
const obj1 = {a: 1, b:2};
const obj2 = JSON.parse('{"__proto__":{"hacked":45}}');
merge(obj1, obj2);
const obj3 = {};
obj3.hacked; // 45
Object 2개를 병합하는 merge 형태의 경우도 Prototype Pollution에 취약하다.
이 경우도 merge 함수의 2번째 인자가 json 형태면 재귀적으로 merge()
를 실행한다.
merge(obj1['__proto__'], {"hacked":45})
재귀적으로 실행되는 merge()
는 위와 같다.
결국, obj1의 __proto__
에 hacked = 45를 세팅하는 코드가 실행된다.
○ clone
function clone(obj) {
return merge({}, obj);
}
const obj1 = JSON.parse('{"__proto__":{"hacked":45}}');
const obj2 = clone(obj1);
const obj3 = {};
obj3.polluted; // 45
merge({}, obj)
를 이용한 Copy 로직도 Prototype Pollution에 취약하다.
특히, merge({}, obj)
로직은 알려진 Javascript 라이브러리에서 많이 나오는 문제점이라고 한다.
reference