설명
신뢰할 수 없는 경로나 파일명을 프로세스 실행 또는 동적 라이브러리 API의 대상으로 사용하면 공격자가 애플리케이션이 로드하거나 실행할 코드를 선택할 수 있습니다. 입력값이 절대 경로인 경우에는 공격자가 준비한 파일을 직접 지정할 수 있고, 파일명이나 상대 경로인 경우에는 PATH, 라이브러리 검색 경로, 현재 디렉터리와 같은 검색 규칙을 악용해 개발자가 의도하지 않은 파일을 선택하게 할 수 있습니다.
Windows에서 CreateProcess*()의 lpApplicationName을 NULL로 전달하면 lpCommandLine의 첫 토큰에서 실행 파일 이름을 해석합니다. 이 상태에서 공백이 포함된 실행 파일 경로를 모호하게 구성하거나 검색 경로에 공격자가 쓸 수 있는 디렉터리가 있으면 다른 실행 파일이 선택될 수 있습니다. LoadLibrary*()도 대상 DLL뿐 아니라 그 DLL의 종속 DLL을 찾을 때 별도의 검색 규칙을 적용하므로, 대상 하나를 고정하는 것만으로 모든 DLL 검색 경로 문제가 해결되지는 않습니다.
이 문제는 셸이 명령 문자열을 해석하는 운영체제 명령어 주입과 구분됩니다. execve()나 CreateProcessW()는 기본적으로 셸 메타문자를 해석하지 않지만, 외부 입력이 실행 파일 자체를 선택한다면 공격자 코드가 실행될 수 있습니다. 반대로 실행 파일을 고정했더라도 신뢰할 수 없는 인자가 대상 프로그램의 옵션이나 문법으로 해석되는 문제는 별도로 검토해야 합니다.
잠재적 영향
- 애플리케이션 계정 권한으로 임의 실행 파일이나 동적 라이브러리 코드가 실행될 수 있습니다.
- 높은 권한의 서비스에서는 권한 상승, 보안 경계 우회 또는 시스템 전체 침해로 이어질 수 있습니다.
- DLL 사이드로딩이나 검색 경로 하이재킹을 통해 지속성을 확보하거나 정상 프로그램의 동작을 바꿀 수 있습니다.
- 악성 또는 호환되지 않는 대상을 실행해 민감정보를 유출하거나 서비스를 중단시킬 수 있습니다.
해결 방법
외부 입력은 실행 경로가 아니라 소수의 업무상 식별자로만 받고, 각 식별자를 신뢰할 수 있는 코드나 설정에 저장된 고정 대상에 매핑합니다. 다음 보호를 함께 적용합니다.
- 실행 파일과 라이브러리에는 신뢰할 수 있는 코드나 설정에 고정한 명시적 전체 경로를 사용합니다. 경로, 파일명 또는 확장자를 외부 입력으로 직접 받지 않습니다.
- 허용된 식별자와 정확히 일치하는 경우에만 코드에 고정된 경로를 선택하고 나머지는 거부합니다. 경로 정규화, 확장자 검사 또는
sanitize_path()같은 함수 이름만으로 실행 권한을 부여하지 않습니다. - POSIX에서는 고정된 절대 경로를
execve()나posix_spawn()에 전달하고, 인자 배열과 최소한의 신뢰할 수 있는 환경을 별도로 구성합니다. 대상 선택에PATH를 사용하는execvp(),execvpe(),execlp(),posix_spawnp()는 피합니다. dlopen()또는dlmopen()에는 신뢰할 수 있는 전체 경로를 전달하고, 대상과 종속 라이브러리의 검색 경로·RPATH·RUNPATH·환경 변수 설정을 함께 검토합니다.- Windows에서는
lpApplicationName에 고정된 실행 파일의 전체 경로를 명시하고,lpCommandLine은 API가 요구하는 쓰기 가능한 별도 버퍼로 구성합니다. DLL은LoadLibraryExW()와 목적에 맞는 제한적인LOAD_LIBRARY_SEARCH_*플래그를 사용하고, 필요하면SetDefaultDllDirectories()로 프로세스 기본 검색 정책도 제한합니다. - 실행 파일, 라이브러리 및 종속 파일이 있는 디렉터리는 신뢰할 수 없는 사용자가 쓸 수 없도록 권한을 설정합니다. 프로세스 계정에는 최소 권한만 부여합니다.
- 코드 서명이나 해시 검증은 심층 방어로 사용할 수 있지만, 신뢰할 수 있는 경로와 파일 시스템 권한을 대신하지는 않습니다. 검증 후 실행 사이에 파일이 바뀌는 경쟁 조건도 고려해야 합니다.
예시
C
변경 전
#include <dlfcn.h>
#include <stddef.h>
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
void *handle = dlopen(argv[1], RTLD_NOW);
if (handle == NULL) {
return 1;
}
dlclose(handle);
return 0;
}
argv[1]이 로드할 라이브러리 경로를 직접 결정하므로 공격자가 애플리케이션 권한으로 로드될 코드를 선택할 수 있습니다.
변경 후
#include <dlfcn.h>
#include <stddef.h>
#include <string.h>
static const char *plugin_path(const char *selector) {
if (strcmp(selector, "image") == 0) {
return "/opt/example/lib/image-plugin.so";
}
if (strcmp(selector, "report") == 0) {
return "/opt/example/lib/report-plugin.so";
}
return NULL;
}
int main(int argc, char **argv) {
if (argc != 2) {
return 2;
}
const char *path = plugin_path(argv[1]);
if (path == NULL) {
return 2;
}
void *handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (handle == NULL) {
return 1;
}
dlclose(handle);
return 0;
}
외부 입력은 image와 report라는 식별자 중 하나를 선택할 뿐이며, 실제 라이브러리 경로는 코드에 고정되어 있습니다. 배포 환경에서는 이 디렉터리와 두 라이브러리의 종속 파일도 신뢰할 수 없는 사용자가 변경할 수 없어야 합니다.
고정된 프로세스를 실행할 때도 대상 경로와 인자를 분리합니다.
#include <spawn.h>
#include <stddef.h>
int run_worker(char *job_id) {
char *const arguments[] = {
"worker", "--job-id", job_id, NULL
};
char *const environment[] = {
"LANG=C", NULL
};
pid_t child;
return posix_spawn(
&child,
"/opt/example/bin/worker",
NULL,
NULL,
arguments,
environment);
}
이 예시는 실행 파일을 절대 경로로 고정하고 인자와 환경을 별도로 전달합니다. job_id에는 애플리케이션의 업무 형식에 맞는 길이·문자·값 검증도 적용해야 하며, 대상 프로그램이 인자를 옵션이나 다른 문법으로 재해석하는지 별도로 확인해야 합니다.
C++
변경 전
#include <dlfcn.h>
#include <iostream>
#include <string>
int main() {
std::string path;
if (!std::getline(std::cin, path)) {
return 2;
}
void *handle = ::dlopen(path.c_str(), RTLD_NOW);
if (handle == nullptr) {
return 1;
}
::dlclose(handle);
return 0;
}
std::string에 저장하거나 c_str()로 변환해도 입력값에 실행 권한이 부여되지는 않습니다. 이 코드는 표준 입력이 지정한 경로의 라이브러리를 그대로 로드합니다.
변경 후
#include <dlfcn.h>
#include <iostream>
#include <string>
#include <string_view>
static const char *plugin_path(std::string_view selector) {
if (selector == "image") {
return "/opt/example/lib/image-plugin.so";
}
if (selector == "report") {
return "/opt/example/lib/report-plugin.so";
}
return nullptr;
}
int main() {
std::string selector;
if (!std::getline(std::cin, selector)) {
return 2;
}
const char *path = plugin_path(selector);
if (path == nullptr) {
return 2;
}
void *handle = ::dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (handle == nullptr) {
return 1;
}
::dlclose(handle);
return 0;
}
외부 입력은 두 업무 식별자 중 하나만 선택하고 실제 경로는 코드에 고정되어 있습니다. std::filesystem::path로 경로를 표현하거나 canonical()로 해석하는 것만으로는 이 식별자 매핑을 대신할 수 없습니다.
참조
- POSIX.1-2024
exec함수군 - POSIX.1-2024
posix_spawn() - Linux man-pages
dlopen()·dlmopen() - Linux man-pages
exec함수군 - Linux man-pages
execveat() - Microsoft Dynamic-Link Library Security
- Microsoft Dynamic-Link Library Search Order
- Microsoft
LoadLibraryExW() - Microsoft
CreateProcessW() - Microsoft
CreateProcessAsUserW() - Microsoft
CreateProcessWithLogonW() - Microsoft
CreateProcessWithTokenW() - ISO/IEC 9899:2024 Programming languages — C
- ISO/IEC 14882:2024 Programming languages — C++
- Microsoft C++
main함수와 명령줄 인수 - OWASP ASVS 5.0.0 V2.2.1
- CWE-114: Process Control
- CWE-73: External Control of File Name or Path
- CWE-426: Untrusted Search Path
- CWE-427: Uncontrolled Search Path Element
- OWASP Top 10:2025 A05 Injection