코드 인젝션 (eval/동적 템플릿 컴파일)

사용자 입력을 실행 코드나 템플릿 소스로 평가하는 코드 인젝션

설명

코드 인젝션은 사용자 입력을 코드처럼 평가하거나 템플릿 엔진이 실행할 소스로 넘길 때 발생합니다. eval, new Function, 브라우저의 문자열 기반 타이머나 EJS/Pug/Lodash 등의 동적 템플릿 실행이 대표적인 예입니다. 영향은 엔진의 기능과 실행 권한에 따라 달라지며, 서버에서는 원격 코드 실행(RCE), 브라우저에서는 스크립트 실행(XSS)으로 이어질 수 있습니다. Node.js 타이머는 문자열 콜백을 실행하지 않고 거부합니다.

잠재적 영향

  • 원격 코드 실행(RCE): 서버에서 임의 자바스크립트가 실행되어 시스템 명령 실행, 파일 접근, 내부망 스캔 등이 가능해집니다.
  • 권한 상승: 애플리케이션 권한으로 시스템 자원에 접근하거나 관리자 권한 획득 시도로 이어질 수 있습니다.
  • 데이터 유출/변조: DB 조회·수정, 환경변수·토큰 탈취 등 민감 정보가 노출되거나 변경될 수 있습니다.
  • 크로스 사이트 스크립팅(XSS): 템플릿 결과가 브라우저에 전달될 때 스크립트가 삽입·실행되어 사용자 세션 탈취가 발생할 수 있습니다.
  • 서비스 거부(DoS): 무한 루프·대량 메모리 사용 등의 코드가 실행되어 서비스가 중단될 수 있습니다.

해결 방법

  • 사용자 입력을 eval, new Function, vm.runIn* 같은 코드 실행 API에 전달하지 마세요.
  • 템플릿은 신뢰하는 고정 소스만 컴파일·렌더링하고, 사용자 입력은 데이터로 전달하세요. 자동 이스케이프를 유지하고, EJS의 <%- %> 같은 비이스케이프 출력은 신뢰하지 않는 값에 사용하지 마세요.
  • 동적 동작이 필요하면 허용 목록에서 미리 정의한 연산이나 함수를 선택하세요.
  • 타이머에는 고정된 작업을 수행하는 콜백을 전달하고, 콜백 안에서도 입력을 코드로 평가하지 마세요.
  • 입력을 검증하고 실제 출력 맥락에 맞는 HTML·URL·JavaScript 인코딩을 적용하세요.
  • Node.js의 vm 모듈을 신뢰할 수 없는 코드에 대한 보안 경계로 사용하지 마세요.

예시

변경 전

javascript
// 비준수 예시: 사용자 입력을 코드로 실행 + 사용자 템플릿을 그대로 렌더링
const express = require("express");
const ejs = require("ejs");
const app = express();
app.use(express.json());

app.get("/calc", (req, res) => {
  const expr = req.query.expr; // 예: "process.env" 또는 악성 코드
  // BAD: 사용자 입력을 코드로 평가
  const result = eval(expr);
  res.send(String(result));
});

app.post("/preview", (req, res) => {
  const userTpl = req.body.tpl; // 사용자가 보낸 템플릿 문자열
  // BAD: 사용자 템플릿을 직접 렌더링 (SSTI)
  const html = ejs.render(userTpl, { name: req.body.name });
  res.send(html);
});

app.get("/wait", (req, res) => {
  // BAD: 타이머 콜백 안에서 사용자 입력을 코드로 평가
  setTimeout(() => eval(req.query.code), 10);
  res.send("scheduled");
});

app.listen(3000);

변경 후

javascript
// 준수 예시: 허용 목록 기반 처리 + 고정 템플릿 + 안전한 타이머 사용
const express = require("express");
const ejs = require("ejs");
const app = express();
app.use(express.json());

// 1) 연산 허용 목록 기반 안전 계산
const OPS = new Map([
  ["add", (a, b) => a + b],
  ["sub", (a, b) => a - b],
  ["mul", (a, b) => a * b],
]);
app.get("/calc", (req, res) => {
  const op = req.query.op; // add/sub/mul 중 하나
  const a = Number(req.query.a);
  const b = Number(req.query.b);
  const operation = typeof op === "string" ? OPS.get(op) : undefined;
  if (!operation || !Number.isFinite(a) || !Number.isFinite(b)) {
    return res.status(400).send("invalid input");
  }
  const result = operation(a, b); // 코드 평가 없이 함수 매핑 사용
  res.send(String(result));
});

// 2) 고정 템플릿만 사용하고 데이터로 값 전달 (자동 이스케이프 유지)
const PROFILE_TPL = "<h1>User</h1><p>Name: <%= name %></p>";
app.post("/preview", (req, res) => {
  const name = typeof req.body.name === "string" ? req.body.name : "";
  const html = ejs.render(PROFILE_TPL, { name }); // 사용자 입력은 데이터로만 전달
  res.send(html);
});

// 3) 타이머에는 함수(콜백)만 전달
app.get("/wait", (req, res) => {
  const ms = Math.min(Number(req.query.ms) || 0, 5000);
  setTimeout(() => {
    // 수행할 작업을 명확한 함수로 정의 (문자열 코드 금지)
  }, ms);
  res.send("scheduled");
});

app.listen(3000);

변경 전에는 eval과 사용자 제공 템플릿으로 서버 코드를 실행할 수 있습니다. Node.js의 타이머 예시도 문자열 콜백 대신 콜백 안의 eval로 같은 위험을 보여 줍니다.

변경 후에는 Map에서 허용된 연산을 선택하고 유한한 숫자만 받습니다. EJS에는 고정 템플릿과 데이터 값을 분리해 전달하고, 타이머도 고정된 콜백만 실행합니다. 애플리케이션에 필요한 연산 범위와 요청량 제한은 별도로 적용하세요.

참조