설명
Zip Slip은 압축 파일의 엔트리 경로(entryName, fileName 등)를 검증하지 않고 파일 시스템 경로로 사용해 쓸 때 발생하는 취약점입니다. 공격자는 엔트리 이름에 "../"(디렉터리 트래버설) 또는 절대 경로(C:, /tmp 등)를 넣어, 의도된 추출 디렉터리 밖의 임의 위치에 파일을 생성하거나 덮어쓸 수 있습니다. 악용 시 웹 루트에 스크립트를 심어 원격 코드 실행을 유발하거나, 시스템/애플리케이션 설정 파일을 덮어써 서비스 장애를 일으킬 수 있습니다.
잠재적 영향
- 임의 파일 쓰기/덮어쓰기: 공격자가 애플리케이션 권한으로 임의 경로에 파일을 생성·수정해 웹셸 업로드, 설정 변조를 유발할 수 있음.
- 원격 코드 실행: 웹 루트, 크론/시작 스크립트 경로 등에 파일을 쓰면 코드가 실행될 수 있음.
- 서비스 거부(DoS): 중요한 구성 파일, 바이너리, 로그를 덮어써 서비스 중단 또는 장애 유발 가능.
- 데이터 무결성 훼손: 애플리케이션 데이터/로그 변조로 추적 회피 및 보안 가시성 저하.
해결 방법
- 경로 검증: 엔트리 경로를
path.normalize한 뒤 절대 경로 여부(path.isAbsolute)와 상위 디렉터리 요소를 확인하고,path.resolve로 계산한 최종 경로가 목적지 디렉터리 내부인지path.relative로 검사하세요. - 우회 차단: Windows 드라이브 표기(예: C:)를 거부하고, ZIP 외부 속성의 UNIX 파일 형식을 확인해 심볼릭 링크 엔트리를 건너뛰세요.
- 허용 목록 기반: 필요한 확장자/디렉터리만 추출(allowlist)하고 나머지는 스킵하세요.
- 파일 생성 정책: 신뢰할 수 있는 부모 아래에 새 전용 추출 디렉터리를 만들고, 기존 파일 덮어쓰기를 금지(open/write 시 'wx')하며, 디렉터리는 안전하게 생성하세요.
- 라이브러리 옵션: zip 처리 라이브러리의 안전 옵션(엔트리 필터, symlink 차단)이 있으면 활성화하세요.
예시
변경 전
javascript
// 비준수: 엔트리 경로를 그대로 사용해 추출 (Zip Slip 취약)
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
function extractUnsafe(zipPath, destDir) {
const zip = new AdmZip(zipPath);
zip.getEntries().forEach((entry) => {
const outPath = path.join(destDir, entry.entryName); // 검증 없음
const data = entry.getData();
// 공격자가 entry.entryName에 "../../../../var/www/html/shell.js"를 넣으면 외부로 탈출 가능
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, data); // 임의 위치 파일 생성/덮어쓰기
});
}
변경 후
javascript
// 준수: 새 전용 디렉터리에서 엔트리 형식과 최종 경로를 검증한 뒤 쓰기
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
function isUnixSymlink(entry) {
// ZIP 외부 속성의 상위 16비트에는 UNIX 파일 모드가 저장됩니다.
const unixMode = entry.header.attr >>> 16;
return (unixMode & 0o170000) === 0o120000;
}
function extractSafe(zipPath, destParent) {
const zip = new AdmZip(zipPath);
const parentReal = fs.realpathSync(destParent);
const destReal = fs.mkdtempSync(path.join(parentReal, "extract-"));
fs.chmodSync(destReal, 0o700);
for (const entry of zip.getEntries()) {
// 1) UNIX 심볼릭 링크 엔트리 거부
if (isUnixSymlink(entry)) continue;
// 2) 경로 정규화
const normalized = path.normalize(entry.entryName);
// 3) 빈 경로/절대 경로/드라이브 문자/상위 디렉터리 요소 차단
if (normalized === "" || normalized === ".") continue;
if (path.isAbsolute(normalized)) continue;
if (normalized.split(path.sep).includes("..")) continue;
if (/^[A-Za-z]:/.test(normalized)) continue;
// 4) 최종 경로 계산 후 목적지 안쪽인지 확인
const targetPath = path.resolve(destReal, normalized);
if (
path.relative(destReal, targetPath) !== ".." &&
!path.relative(destReal, targetPath).startsWith(".." + path.sep) &&
!path.isAbsolute(path.relative(destReal, targetPath))
) {
if (entry.isDirectory) {
fs.mkdirSync(targetPath, { recursive: true, mode: 0o700 });
continue;
}
// 5) 새 전용 디렉터리 안에서만 생성하고 기존 파일은 덮어쓰지 않음
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
const data = entry.getData();
fs.writeFileSync(targetPath, data, { flag: "wx", mode: 0o600 });
}
}
return destReal;
}
설명:
- 변경 전: 엔트리 이름(entryName)을 검증 없이 path.join으로 결합해 파일을 씀으로써, "../"가 포함된 엔트리가 목적지 디렉터리 밖으로 탈출해 임의 파일 생성/덮어쓰기가 가능합니다.
- 변경 후: 신뢰할 수 있는 부모 아래에 새 전용 추출 디렉터리를 만들고, ZIP 외부 속성에서 UNIX 심볼릭 링크 형식인 엔트리를 실제로 거부합니다. 엔트리 경로를 정규화한 뒤 절대 경로·드라이브 문자·상위 디렉터리 요소를 차단하고,
path.resolve와path.relative로 최종 경로의 디렉터리 경계를 확인합니다. 검증을 통과한 파일만wx모드로 새로 생성하므로 기존 파일도 덮어쓰지 않습니다.