설명
system(), popen(), _popen()은 문자열을 명령 처리기에 전달합니다. C++에서 <cstdlib>의 std::system()을 호출하는 경우도 같습니다. 신뢰할 수 없는 입력이 이 문자열에 포함되면 셸 메타문자, 명령 구분자, 리디렉션 또는 추가 인자가 코드로 해석될 수 있습니다. wordexp()도 셸의 단어 확장을 수행하며, WRDE_NOCMD를 지정하지 않으면 명령 치환을 실행할 수 있습니다.
셸 문자를 이스케이프하는 것만으로는 모든 대상 셸과 실행 프로그램의 문법을 안전하게 처리할 수 없습니다. 셸 명령 자체가 차단되더라도 대상 프로그램이 신뢰할 수 없는 값을 옵션으로 해석하는 인자 주입이 남을 수 있습니다. shell_escape()나 sanitize_command_arg() 같은 함수 이름만으로 안전성을 판단해서도 안 됩니다.
잠재적 영향
- 애플리케이션 권한으로 임의의 운영체제 명령이 실행될 수 있습니다.
- 파일 삭제·변조, 민감정보 유출, 내부 시스템 접근 또는 서비스 중단으로 이어질 수 있습니다.
- 높은 권한으로 실행되는 프로세스에서는 시스템 전체가 침해될 수 있습니다.
해결 방법
가능하면 외부 명령 호출을 제거하고 해당 기능을 제공하는 라이브러리 API를 사용합니다. 외부 프로세스가 꼭 필요하다면 다음 원칙을 함께 적용합니다.
- 실행 파일 경로를 코드에서 고정하고
execve(), 적절한posix_spawn()함수 또는 플랫폼의 프로세스 API를 사용합니다. - 명령 문자열을 조립하지 말고 실행 파일과 인자를 분리합니다.
- 각 인자를 대상 프로그램의 데이터 형식에 맞는 허용 목록으로 검증하고, 선행
-나--처리 등 옵션 주입 가능성도 확인합니다. - 필요한 환경 변수만 신뢰할 수 있는 값으로 구성하고 오류를 처리합니다.
wordexp()가 불가피하면WRDE_NOCMD를 포함하고 반환값을 확인합니다. 이 플래그는 명령 치환을 요청한 입력을WRDE_CMDSUB오류로 거부하지만, 다른 단어 확장까지 일반적으로 안전하게 만드는 정제 함수는 아닙니다.
예시
C
변경 전
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
return system(argv[1]);
}
변경 후
#include <ctype.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static int valid_label(const char *value) {
size_t length = strlen(value);
if (length == 0 || length > 64) {
return 0;
}
for (size_t i = 0; i < length; ++i) {
unsigned char ch = (unsigned char)value[i];
if (!isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
return 0;
}
}
return 1;
}
int main(int argc, char **argv) {
if (argc != 2 || !valid_label(argv[1])) {
return 2;
}
char *const args[] = {"printf", "Archive: %s\n", argv[1], NULL};
execv("/usr/bin/printf", args);
perror("execv");
return 1;
}
설명:
- 변경 전:
argv[1]전체가 셸이 해석하는 명령 문자열로 전달됩니다. 따옴표를 추가해도 모든 셸 구문을 안전하게 만들지는 못합니다. - 변경 후: 실행 파일과 형식 문자열을 고정하고 사용자 값을 별도 인자로 전달합니다. 허용 목록 검증은 이 값에 허용되는 업무 형식도 제한합니다.
wordexp()가 필요한 경우에는 명령 치환을 명시적으로 금지하고 모든 오류를 처리합니다.
#include <wordexp.h>
int expand_without_commands(const char *input, wordexp_t *words) {
int result = wordexp(input, words, WRDE_NOCMD | WRDE_UNDEF);
if (result != 0) {
if (result == WRDE_NOSPACE) {
wordfree(words);
}
return -1;
}
return 0;
}
성공하면 호출자가 결과를 사용한 뒤 wordfree()로 해제합니다. WRDE_NOSPACE 오류에서도 부분 할당된 메모리를 해제해야 합니다.
C++
변경 전
#include <cstdlib>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
return std::system(argv[1]);
}
변경 후
#include <cctype>
#include <cstdio>
#include <cstring>
#include <unistd.h>
static bool valid_label(const char *value) {
std::size_t length = std::strlen(value);
if (length == 0 || length > 64) {
return false;
}
for (std::size_t i = 0; i < length; ++i) {
unsigned char ch = static_cast<unsigned char>(value[i]);
if (!std::isalnum(ch) && ch != '.' && ch != '_' && ch != '-') {
return false;
}
}
return true;
}
int main(int argc, char **argv) {
if (argc != 2 || !valid_label(argv[1])) {
return 2;
}
char program[] = "/usr/bin/printf";
char format[] = "Archive: %s\n";
char *const args[] = {program, format, argv[1], nullptr};
execv(program, args);
std::perror("execv");
return 1;
}
std::system() 대신 고정된 실행 파일을 직접 호출하고 사용자 값을 별도 인자로 전달합니다. 이 예시는 C++23과 POSIX.1-2024 환경에서 유효하며, 허용 목록은 사용자 값의 업무 형식을 함께 제한합니다.
참조
- POSIX.1-2024
system() - POSIX.1-2024 셸 명령 언어
- POSIX.1-2024
wordexp() - POSIX.1-2024
exec함수군 - POSIX.1-2024
posix_spawn() - Microsoft
_popen,_wpopen - Microsoft C++ 표준 라이브러리
<cstdlib> - Microsoft
system,_wsystem - ISO/IEC 14882:2024 Programming languages — C++
- OWASP OS Command Injection Defense Cheat Sheet
- OWASP ASVS 5.0.0 V1.2.5
- SEI CERT C ENV33-C: Do not call
system() - CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP Top 10:2025 A05 Injection
- OWASP Top 10:2021 A03 Injection