설명
사용자 입력을 window, globalThis, global 같은 전역 객체의 함수 이름으로 사용하면 의도하지 않은 함수를 호출할 수 있습니다. eval은 전달한 문자열을 실행하고, Function은 나중에 호출될 수 있는 함수를 생성합니다. 실행할 문자열까지 공격자가 제어하면 해당 실행 환경의 권한으로 임의 코드를 실행할 수 있습니다.
잠재적 영향
- 원격 코드 실행(RCE): 전역 객체의 eval/Function 호출을 유도해 임의 JavaScript 코드를 실행할 수 있습니다.
- 데이터 유출: 실행된 코드로 쿠키, 토큰, 환경변수 등 민감 정보를 읽고 외부로 전송할 수 있습니다.
- 데이터 변조/권한 남용: 애플리케이션 상태 변경, 관리자 기능 호출 등 권한 외 동작을 수행할 수 있습니다.
- 서비스 거부(DoS): 무한 루프나 대량 연산 코드를 실행해 자원을 고갈시킬 수 있습니다.
해결 방법
- 전역 객체(window/globalThis/global)나 함수 객체에 대해 사용자 입력으로 메서드 이름을 동적 호출하지 마세요.
- 허용 목록(allowlist) 기반 디스패치: 전용 API 객체 또는 Map에 허용된 함수만 등록하고,
Object.hasOwn등으로 허용된 키인지 확인한 뒤 호출하세요. - 전역 객체 접근이 불가피한 경우에도
encodeURIComponent,decodeURIComponent처럼 코드 생성이나 타이머 실행을 하지 않는 좁은 목록만 서버 코드에 상수로 두고 허용하세요. - 기본 거부(default deny): 목록에 없는 이름은 즉시 거부하고, 안전한 기본 동작을 사용하세요.
- eval/Function 금지:
eval,Function,setTimeout,setInterval,execScript같은 코드 실행/문자열 실행 가능 함수는 허용 목록에 포함하지 마세요. JSON 파싱, 명시적 조건 분기, 안전한 API 맵 등 대체 수단을 사용하세요. - 방어 심화: 브라우저에서는 CSP에서 'unsafe-eval'을 금지하고, Node.js에서는 가능하면 V8 옵션(--disallow-code-generation-from-strings)과 런타임/빌드 정책으로 코드 생성 함수를 제한하세요.
예시
변경 전
javascript
// Browser 예시
// URL 해시값(# 뒤)을 함수 이름으로 받아 전역 객체에서 호출
// 예: https://site.example/#eval -> window["eval"]("alert('pwned')") 실행
window.addEventListener('load', () => {
const name = location.hash.slice(1); // 사용자 제어
window[name]("alert('pwned')"); // 취약: eval/Function 호출 가능
});
// Node.js (Express) 예시
const express = require('express');
const app = express();
app.get('/do', (req, res) => {
const action = req.query.action; // 사용자 제어
// 예: /do?action=eval -> 예시 문자열이 코드로 실행되어 전역 상태 변경
globalThis[action]("globalThis.exampleResult = 1"); // 취약
res.send('done');
});
변경 후
javascript
// 안전한 디스패치: 전용 API 객체 + 허용 목록 + 키 검증
// Object.create(null)로 프로토타입에서 상속된 멤버를 제거
const API = Object.create(null);
API.ping = (d) => 'pong';
API.upper = (d) => String(d || '').toUpperCase();
function callApi(name, data){
// 키 검증: 존재 여부 + 타입 확인
if (!Object.hasOwn(API, name) || typeof API[name] !== 'function') {
throw new Error('invalid action');
}
return API[name](data);
}
// Browser 예시
window.addEventListener('load', () => {
const name = new URL(location.href).searchParams.get('action');
try {
const result = callApi(name, 'hello');
console.log(result);
} catch (e) {
console.warn('blocked');
}
});
// Node.js (Express) 예시
const express = require('express');
const app = express();
app.get('/do', (req, res) => {
try {
const result = callApi(req.query.action, req.query.data);
res.json({ ok: true, result });
} catch {
res.status(400).json({ ok: false, error: 'invalid action' });
}
});
// 제한적인 전역 함수 허용 목록 예시: 코드 실행 함수는 포함하지 않음
const SAFE_GLOBALS = Object.freeze(["encodeURIComponent", "decodeURIComponent"]);
app.get('/encode', (req, res) => {
const action = req.query.action;
if (!SAFE_GLOBALS.includes(action)) {
return res.status(400).send('invalid action');
}
res.send(globalThis[action](String(req.query.value || '')));
});
설명:
- 변경 전: 사용자 입력으로 전역 함수를 선택합니다.
eval을 선택하면 예시에 고정된 문자열이 실행됩니다.Function은 함수만 생성하며 본문은 생성된 함수를 호출할 때 실행됩니다. 실행 문자열도 외부 입력이라면 임의 코드 실행으로 이어질 수 있습니다. - 변경 후: 전용 API 컨테이너에 허용된 함수만 등록한 뒤 Object.hasOwn과 typeof로 키와 타입을 검증합니다. 전역 객체 접근이 필요한 예외적인 경우에도 코드 생성 함수가 아닌 좁은 상수 목록만 허용해야 합니다. 목록에 없는 이름은 기본 거부하며, 프로토타입 체인을 제거(Object.create(null))해 우회 가능성을 낮췄습니다.