외부 명령 출력이 길어지자 프로세스가 조용히 죽은 이유
문제 발생
이미지 변환 결과를 외부 명령으로 처리하는 코드가 큰 파일에서만 실패했습니다.
exec(`convert ${input} -resize 800x ${output}`, (err, stdout) => { ... });Error: stdout maxBuffer length exceeded게다가 파일명에 공백이나 ;가 들어오면 명령 자체가 이상하게 해석됐습니다.
원인 분석
두 가지 문제가 한 함수에 겹쳐 있습니다.
첫째, 버퍼링. Node.js 문서는 child_process.exec()를 셸을 띄운 뒤 그 셸 안에서 명령을 실행하고 생성된 출력을 버퍼링하는 함수로 설명합니다. 그리고 maxBuffer 옵션의 기본값은 **1MB(1024 * 1024)**이며, 초과하면 자식 프로세스가 종료되고 출력이 잘립니다.
spawn()은 다릅니다 — 스트림(subprocess.stdout)을 돌려주므로 전부 메모리에 담지 않고 흘려보낼 수 있습니다.
둘째, 셸 인젝션. 문서의 경고가 강합니다 — 이 함수에 정제되지 않은 사용자 입력을 절대 전달하지 마십시오. 셸 메타문자를 포함한 어떤 입력이든 임의 명령 실행을 유발하는 데 사용될 수 있습니다.
exec는 셸을 거치므로 ;, |, $()가 전부 의미를 갖습니다. 파일명 하나로 서버에서 임의 명령이 실행될 수 있습니다.
해결 방안
- 인자를 배열로 넘기는
spawn/execFile을 씁니다. 셸을 거치지 않으므로 메타문자가 그냥 문자열이 됩니다.
const child = spawn("convert", [input, "-resize", "800x", output]);- 출력이 크면 스트림으로 처리합니다. 파일로 쓰거나 파이프로 넘기면 메모리에 쌓이지 않습니다.
child.stdout.pipe(createWriteStream(output));-
exec가 편한 경우에도 한계를 인지합니다. 출력이 작다고 확신할 때만 쓰고, 필요하면maxBuffer를 명시적으로 올립니다 — 다만 그건 메모리를 그만큼 쓰겠다는 선언입니다. -
종료 코드와 stderr를 확인합니다. 자식이 실패했는데 애플리케이션은 성공으로 넘어가는 실수가 흔합니다.
close이벤트의 코드가 0인지 반드시 봅니다. -
shell: true를 습관적으로 켜지 않습니다. 문서가execFile에 대해서도 같은 경고를 반복합니다 —shell옵션이 켜져 있으면 정제되지 않은 사용자 입력을 전달하지 마십시오. -
외부 명령 자체가 필요한지 되묻습니다. 라이브러리로 처리할 수 있으면 프로세스 생성 비용과 인젝션 표면이 함께 사라집니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.