설명
libarchive의 archive_entry_pathname(), archive_entry_hardlink(), archive_entry_symlink() 계열 함수가 반환하는 엔트리 경로명과 링크 대상은 아카이브가 제공하는 데이터입니다. 이 값을 파일 시스템 API에 그대로 전달하면 절대 경로, .. 구성 요소 또는 링크 리디렉션을 통해 의도한 추출 디렉터리 밖을 가리킬 수 있습니다.
archive_entry_sourcepath()는 의미가 다릅니다. 이 함수는 archive_read_disk가 아카이브를 만들 때 사용하는 로컬 파일 시스템의 원본 경로를 반환하며, 그 값 자체가 아카이브에 저장되는 것은 아닙니다.
잠재적 영향
- 프로세스 권한으로 임의 파일을 읽거나 파일과 디렉터리를 생성·교체·이름 변경·삭제할 수 있습니다.
- 설정 파일, 실행 파일, 배포 경로 또는 다른 사용자의 데이터를 변조할 수 있습니다.
해결 방법
추출 위치를 신뢰할 수 있는 고정 디렉터리로 정하고, 가능하면 libarchive가 제공하는 다음 세 가지 보호 옵션을 함께 사용합니다.
ARCHIVE_EXTRACT_SECURE_NODOTDOT:..구성 요소가 있는 경로를 거부합니다.ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS: 절대 경로를 거부합니다.ARCHIVE_EXTRACT_SECURE_SYMLINKS: 디스크의 심볼릭 링크 때문에 최종 위치가 바뀌는 엔트리를 거부합니다.
libarchive 3.8.9 문서에 따르면 이 검사들은 기본적으로 활성화되지 않습니다. archive_read_extract()에 플래그를 전달하거나, archive_read_extract2()가 사용하는 archive_write_disk 객체에 archive_write_disk_set_options()로 같은 플래그를 설정해야 합니다.
ARCHIVE_EXTRACT_SAFE_WRITES는 임시 파일을 만든 뒤 이름을 변경하여 부분 쓰기를 피하는 옵션일 뿐, 경로 격리를 보장하지 않습니다. 기존 파일 덮어쓰기를 막는 옵션도 경로 격리와는 별개의 보호 수단입니다.
직접 추출 로직을 구현한다면 엔트리 경로와 저장된 하드 링크·심볼릭 링크 대상 모두에서 대상 플랫폼의 절대 경로와 .. 구성 요소를 거부하고, 미리 연 추출 디렉터리를 기준으로 조회 시점에 격리를 강제하는 운영체제 API를 사용합니다. 링크를 복원할 필요가 없다면 해당 엔트리를 거부하는 편이 안전합니다. 문자열 정규화 후 접두사만 비교하거나 basename()만 호출하는 방식은 링크, 플랫폼별 경로 구분자 및 루트 경로를 안전하게 처리하지 못합니다. C++의 std::filesystem::path::filename(), lexically_normal(), canonical(), weakly_canonical()도 그 결과를 파일 API에 전달하는 시점의 경로 격리와 링크 경합 방지를 단독으로 보장하지 않습니다. 디렉터리 구조가 필요 없다면 아래 예시처럼 경로 구분자가 없는 단일 파일 이름만 허용하는 정책이 더 단순합니다.
예시
C
변경 전
#include <archive_entry.h>
#include <stdio.h>
void extract_unsafe(struct archive_entry *entry) {
const char *name = archive_entry_pathname(entry);
FILE *out = fopen(name, "wb");
if (out != NULL) {
fclose(out);
}
}
변경 후
#include <archive.h>
#include <archive_entry.h>
int extract_entry(struct archive *reader, struct archive_entry *entry) {
const int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
ARCHIVE_EXTRACT_SECURE_SYMLINKS;
return archive_read_extract(reader, entry, flags);
}
위 코드는 프로세스의 현재 작업 디렉터리가 신뢰할 수 있는 추출 루트로 설정되어 있다는 전제에서 libarchive의 경로 검사를 활성화합니다. 오류 반환값을 무시하지 말고 해당 엔트리의 추출을 중단해야 합니다.
디렉터리 엔트리를 지원하지 않는 수동 추출 정책은 다음과 같이 구성할 수 있습니다.
#if defined(__APPLE__)
#define _DARWIN_C_SOURCE
#else
#define _POSIX_C_SOURCE 200809L
#endif
#include <archive_entry.h>
#include <fcntl.h>
#include <string.h>
int open_flat_entry(int output_dir_fd, struct archive_entry *entry) {
const char *name = archive_entry_pathname(entry);
if (name == NULL || name[0] == '\0' ||
strchr(name, '/') != NULL || strchr(name, '\\') != NULL ||
strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
return -1;
}
return openat(output_dir_fd, name,
O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
}
설명:
- 변경 전: 아카이브 내부 파일 이름을 그대로 파일 생성 경로로 사용합니다.
- 변경 후: 첫 번째 예시는 libarchive의 절대 경로, 상위 디렉터리 및 심볼릭 링크 보호를 모두 명시적으로 활성화합니다. 두 번째 예시는 경로 구분자와
./..를 거부한 뒤 미리 안전하게 연output_dir_fd를 기준으로openat()을 호출하고, 기존 파일과 마지막 경로 구성 요소의 심볼릭 링크를 거부합니다.
C++
변경 전
#include <archive_entry.h>
#include <filesystem>
#include <fstream>
void extract_unsafe_cpp(struct archive_entry *entry) {
const auto leaf = std::filesystem::path(
archive_entry_pathname(entry)).filename();
std::ofstream output{leaf};
}
filename()은 문자열에서 마지막 구성 요소를 고를 뿐, 고정된 추출 루트를 설정하거나 기존 심볼릭 링크를 따라가지 않도록 파일을 열지는 않습니다.
변경 후
#include <archive.h>
#include <archive_entry.h>
int extract_entry_cpp(struct archive *reader, struct archive_entry *entry) {
constexpr int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
ARCHIVE_EXTRACT_SECURE_SYMLINKS;
return archive_read_extract(reader, entry, flags);
}
이 예시도 현재 작업 디렉터리가 신뢰할 수 있는 추출 루트라는 전제에서 사용하며, libarchive가 반환한 오류를 실패로 처리해야 합니다.