프로토타입, 오염이냐 상속이냐
2022.09.11
Prototype이란?
JavaScript에서는 모든 객체들이 메소드와 속성들을 상속 받기 위한 템플릿으로써 prototype
객체를 가진다. 그리고 이 prototype
객체 또한 상위 prototype
객체로부터 메소드와 속성을 상속받을 수 있고, 그 상위 객체도 마찬가지다. 즉, 다른 객체에 정의된 메소드와 속성을 한 객체에서 사용할 수 있도록 해준다.
instance.method()
가 호출될 경우,instance
에method()
가 정의되어 있는지 확인한다.- 없을 경우,
instance
의prototype
객체에method()
가 정의되어 있는지 확인한다. - 또 없으면, 상위
prototype
확인...한 객체의 메소드와 속성들이 다른 객체로 복사되는 것이 아닌,
prototype
체인을 타고 올라가며 접근할 뿐이다.
객체의 Prototype
const dog = {};
dog
를 아무 속성 없도록 생성했지만, 이 강아지는 단순히 비어있는 친구가 아니다.
console.log(dog)
를 실행해보면 어떤 결과가 나올까?
분명 아무 속성도 주지 않았는데, 여러가지가 있는 것을 확인해볼 수 있다. 이 속성이 어디서 온 것인에 대한 답을 prototype
에서 찾아볼 수 있다.
JavaScript에서 사용하는 class의 개념은 함수에서 시작했다. 함수 그 자체는 class의 constructor 역할을 한다. 따라서 아래와 같이 코드를 작성할 수 있다.
function Cat() {
this.says = function() { return "meow"; };
}
const myCat = new Cat();
Cat.prototype.canFly = function() { return false; };
console.log(myCat);
myCat
instance에서 사용 가능한 함수들은 prototype
에 선언되게 된다.
Runtime에 prototype
의 내부 값을 변경할 수 있다는 점과, prototype
chaining을 활용하면, 상속을 구현할 수도 있고, exploit 하는데 활용할 수도 있을 것이다.
Prototype Inheritance
class
선언을 사용하지 않고, 함수를 사용해서 아래와 같이 Animal
객체를 생성해보자.
// 속성 정의는 생성자 안에서
function Animal(name, legs) {
this.name = name;
this.legs = legs;
}
// 메소드 정의는 prototype을 사용해서
Animal.prototype.say = function(word) { return word; };
Animal.prototype.toString = function() { return this.name + " with "+this.legs+" legs."; };
위의 Animal
을 상속하는 Cat
객체를 만들고 싶다면 아래와 같이 하면 된다.
function Cat(name, legs, canFly) {
Person.call(this, name, legs);
// 여기서 this는 Cat을 가리킨다.
this.canFly = canFly;
}
call()
은 이미 할당되어있는 다른 객체의 함수/메소드를 현재 객체(this
)에 재할당할 때 사용한다.
여기서의 문제 : Cat
이 자기 자신에 대한 참조만 가지고 있다는 것
즉, Animal에 작성되어있는 say()
/ toString()
메소드가 Cat
을 통해서는 접근 불가능하다. 따라서 아래와 같이 Cat
의 prototype
이 Animal
의 prototype
을 참조하도록 해야한다.
Cat.prototype = Object.create(Animal.prototype);
여기서의 문제: Cat
의 constructor가 Animal
로 되어있다는 것
Cat.prototype에 Animal.prototype을 상속받은 객체를 할당했기 때문이고, 아래와 같이 consturcot를 Cat
으로 지정해주어야한다.
Cat.prototype.constructor = Cat;
만약, Cat
이 say(word)
할 때 마지막에 항상 "meow" 를 붙이는 식으로 말하게 하고 싶다면 아래와 같이 메소드를 다시 지정하면 된다.
Cat.prototype.say = function(word) { return word + " meow"; };
class 문법은 prototype을 통해 객체를 생성한 것과 다를바 없지만, prototype을 통해 method를 정의하는 것보다 코드의 가독성/유지보수 측면에서 편리하다.
Prototype Pollution
function isObject(v) { return typeof v === "object"; }
function merge(a, b) {
for (const key in b) {
if (isObject(a[key]) && isObject(b[key])) {
merge(a[key], b[key]);
} else {
a[key] = b[key];
}
}
return a;
}
const express = require("express");
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
const userObject = {};
/**
* 🔨 userObject 뚝딱뚝딱 🔨
*/
if (userObject.isAdmin) res.end("<h1>you are admin</h1>");
else res.end("<h1>you are not admin</h1>");
});
// #1
app.post("/", (req, res) => {
const result = { status: 200 };
merge(result, req.body);
res.json(result);
});
// #2
app.post("/dos", (req, res) => {
merge({}, req.body);
res.end("dos attack");
});
app.listen(3000);
위와 같은 express 서버가 있다고 해보자.
GET: /
:userObject
를 생성하고,isAdmin
속성 여부에 따라 다른 결과를 보여준다.POST: /
:req.body
로 전달받은 내용과result
에 있는 정보를 병합시킨 후 사용자에게 다시 전달해준다.
1. 만약, 악의적인 사용자가 아래와 같은 내용을 '/'
로 POST request를 보냈다면?
{
"__proto__": {
"isAdmin": true
}
}
악의적인 사용자가 요청을 보내기 전에는 모든 사용자가 '/'
로 접근했을 때, you are not admin
메세지를 봤겠지만, 이제는 모든 사용자가 you are admin
이라는 화면을 보게 된다.
이러한 현상이 생긴 두 가지 원인을 서버 코드에서 찾아볼 수 있다.
merge
함수에서 적절하지 않은key
값은 건너뛰도록 했더라면... lodash에서는 어떤 value에 대해, 그 key가 적절한지 다음과 같이 확인한다. 링크- 최초에
userObject
를 선언할 때isAdmin: false
로 초기화 시켰더라면...userObject.isAdmin
이 지정되어있지 않다보니, prototype chaining에 의해merge
함수에 의해 설정된Object.isAdmin
의 값을 갖게 된다.
2. POST: /dos
로 아래와 같은 내용을 보낸다면?
{
"__proto__": {
"toString": "아무 값이나 입력해버려",
"valueOf": "DoS 공격하기 위함이니까"
}
}
이 다음번의 모든 request들은 500 Internal Server Error
를 겪게 된다. prototype pollution은 DoS(Denial of Service) 공격에 사용될 수도 있다.
Prototype Pollution이 일어나는 경우
prototype pollution을 야기하는 경우를 세가지로 나눠볼 수 있다.
1. Object recursive merge
위에서 작성한 merge
함수가 그 예시다. 최상위 객체인 Object
의 prototype
값에 접근하여 모든 객체의 template을 수정하게 된다.
2. Property definition by path
어떤 라이브러리들에서는 객체의 값을 접근할 때 path
를 통해 접근할 수 있게 하는데, 이 때 path
값으로 __proto__.key
를 전달하여 특정 value
를 set 할 수 있다.
3. Object clone
function clone(obj) { return merge({}, obj); }
같은 식으로 코드를 작성하면, obj
에 수정된 prototype의 key, value로 template을 망가트릴 수도 있다.
이러한 Pollution을 막기 위해서는?
- Prototype을 freeze 시켜버린다.
Object.freeze(Object.prototype);
Object.freeze(Object);
({}).__proto__.something = 1111;
console.log(({}).something); // undefined
- JSON input에 대한 검증 : 불필요한 속성값들에 대한 접근을 reject 시켜버린다.
- Map 을 사용한다. 링크
- prototype이 없는 객체를 생성한다.
const obj = Object.create(null);
console.log(obj.__proto__); // undefined
console.log(obj.constructor); // undefined
'내가 만든 이 객체는 깨끗한가?'
꼭 Prototype Pollution 기법을 이야기하지 않더라도, 내가 만든 객체들간의 상속 관계가 서로를 "오염"시키고 있지는 않은지 생각해볼 수 있을 것 같다.
- 객체간의 책임을 명확히 부여해주었는가?
- SOLID 원칙을 고려했을 때, 위반하는 부분은 어디인가?
- 불필요한 상태를 부여함으로써 객체를 "오염"시키지는 않았는가?
갑작스러울 수 있지만, 상속을 구현할 수 있는 prototype
이 서비스의 취약점이 될 수도 있는 부분은 사람을 참 많이 닮지 않았는가? 서로 서로 협력 관계를 구축하면서도, 한 사람으로 인해 전체 조직이 와해될 수도 있고, 반대로 한 사람으로 인해 모두가 하나가 될 수 있듯이.
For as by one man's disobedience many were made sinners, so by the obedience of one shall many be made righteous. - Romans 5:19
References
- JavaScript Object prototypes -MDN web docs
- Prototype Pollution attack in NodeJS application by Olivier Arteau
- JavaScript 함수 생성자와 클래스의 차이
- https://www.hahwul.com/cullinan/prototype-pollution/
- https://brightsec.com/blog/prototype-pollution/
- https://research.securitum.com/prototype-pollution-rce-kibana-cve-2019-7609/
- https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/
- 객체지향 개발 5대 원리: SOLID