FlawOpen

flawopen.com/명령 주입/Node child_process.exec

Node.js child_process.exec()의 명령 주입 위험

참고 페이지
쉽게 설명하면

명령 문자열을 exec()에 전달하는 것은 문장을 조판 담당자에게 주고 문장 부호로 지시의 경계를 정하게 하는 것과 같습니다. 신뢰할 수 없는 텍스트에 shell 구문이 있으면 shell이 이를 제어 명령으로 해석할 수 있습니다. execFile()은 고정된 프로그램과 개별 인자를 전달하므로 기본적으로 shell 파싱을 거치지 않습니다.

취약
const { exec } = require('node:child_process');

function inspectFile(filename) {
  return exec(`file ${filename}`);
}
수정됨
const { execFile } = require('node:child_process');

const allowed = new Map([
  ['report-1', '/srv/reports/1.pdf'],
  ['report-2', '/srv/reports/2.pdf']
]);

function inspectReport(reportId) {
  const path = allowed.get(reportId);
  if (!path) throw new Error('Unknown report');
  return execFile('/usr/bin/file', ['--', path], {
    shell: false,
    timeout: 3000,
    maxBuffer: 64 * 1024
  });
}

execFile()이 명령 주입 위험을 줄이는 이유

exec()는 shell을 시작하고 명령 문자열을 전달합니다. 코드가 공격자 제어 텍스트를 문자열에 삽입하면 shell은 메타문자를 구문으로 파싱합니다. 이것이 CWE-78에 설명된 명령 주입 경로입니다. execFile()은 인자 배열을 사용해 지정된 실행 파일을 시작하며 기본적으로 shell을 만들지 않으므로 shell 구분자를 shell이 다시 해석하지 않습니다. 예제는 호출자가 제공한 ID도 고정 경로에 매핑합니다. 그렇다고 대상 프로그램까지 안전해지는 것은 아닙니다. 대상 프로그램이 허용하는 인자를 검증하고, 지원하는 경우 --로 옵션 파싱을 끝내며, 신뢰할 수 없는 값에 shell: true를 사용하지 마세요.

위험한 exec() 호출 찾기

exec()와 execSync()를 검색한 뒤 각 명령 문자열의 출처를 요청 매개변수, DB 필드, 파일명, 환경 변수까지 추적하세요. 실행 파일과 인자 경계를 기록합니다. 직접 API를 우선 사용하고, 그렇지 않으면 고정 실행 파일, 검증된 인자, 실행 시간 및 출력 제한을 적용한 execFile()을 사용하세요.

grep -RInE 'exec(Sync)?[[:space:]]*\(' --include='*.js' --include='*.cjs' --include='*.mjs' --include='*.ts' .

동기식 변형

execSync()는 어떤가요?

execSync()도 같은 shell 기반 명령 문자열을 사용하므로 동일한 주입 위험이 있고 이벤트 루프도 차단합니다. execFileSync()는 기본적으로 shell 파싱을 피하지만 실행 파일과 인자는 여전히 검증해야 합니다.

참고 자료

함께 보기 OS 명령어 인젝션(CWE-78): 동작 원리와 모든 언어에서의 예방법 — flawopen.com Node.js exec() vs execFile(): 차이와 보안 Java Runtime.exec()와 Command Injection: 무엇이 다른가?