압축 파일 경로 순회

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

개요

ZipSlip이라고도 하는 압축 파일 경로 순회 취약점은 압축된 보관 파일의 처리와 관련이 있습니다. 이 페이지에서는 이 취약점을 ZIP 형식을 사용하여 설명하나, TAR, RAR, 7z 등의 다른 형식을 처리하는 라이브러리에서도 이와 비슷한 문제가 발생할 수 있습니다.

이 문제가 발생하는 주된 이유는 ZIP 파일 내에서는 각 패킹된 파일이 슬래시나 점과 같은 특수문자의 사용이 허용되는 정규화된 이름으로 저장되기 때문입니다. java.util.zip 패키지의 기본 라이브러리는 보관 파일 항목의 이름에서 디렉터리 순회 문자(../)를 검사하지 않으므로 보관 파일에서 추출된 이름을 타겟팅된 디렉터리 경로와 연결할 때 면밀한 주의를 기울여야 합니다.

ZIP 파일을 추출하는 코드 스니펫이나 라이브러리를 외부 소스에서 가져온 경우에는 유효성을 검사하는 것은 매우 중요합니다. 이러한 라이브러리는 많은 경우 압축 파일 경로 순회에 취약합니다.

영향

압축 파일 경로 순회 취약점은 임의의 파일 덮어쓰기를 달성하는 용도로 사용될 수 있습니다. 영향은 조건에 따라 달라질 수 있으나, 많은 경우 이 취약점으로 인해 코드 실행과 같은 심각한 보안 문제가 발생할 수 있습니다.

완화

이 문제를 완화하려면 각 항목을 추출하기 전에 타겟 경로가 대상 디렉터리의 하위 요소인지 항상 확인해야 합니다. 아래의 코드는 대상 디렉터리가 안전하다고(앱에 의해서만 쓸 수 있고 공격자가 제어하지 않고 있다고) 가정합니다. 안전하지 않을 경우 앱이 심볼릭 링크 공격과 같은 다른 취약점에 노출되었을 수 있습니다.

Kotlin

companion object {
    @Throws(IOException::class)
    fun newFile(targetPath: File, zipEntry: ZipEntry): File {
        val name: String = zipEntry.name
        val f = File(targetPath, name)
        val canonicalPath = f.canonicalPath
        if (!canonicalPath.startsWith(
                targetPath.canonicalPath + File.separator)) {
            throw ZipException("Illegal name: $name")
        }
        return f
    }
}

자바

public static File newFile(File targetPath, ZipEntry zipEntry)
        throws IOException {
    String name = zipEntry.getName();
    File f = new File(targetPath, name);
    String canonicalPath = f.getCanonicalPath();
    if (!canonicalPath.startsWith(
            targetPath.getCanonicalPath() + File.separator)) {
        throw new ZipException("Illegal name: " + name);
    }
    return f;
}

기존 파일을 우발적으로 덮어쓰지 않으려면 추출 프로세스를 시작하기 전에 대상 디렉터리가 비어 있는지도 확인해야 합니다. 그러지 않으면 앱이 비정상 종료될 수 있으며, 심각한 경우 애플리케이션이 손상될 수 있습니다.

Kotlin

@Throws(IOException::class)
fun unzip(inputStream: InputStream?, destinationDir: File) {
    if (!destinationDir.isDirectory) {
        throw IOException("Destination is not a directory.")
    }
    val files = destinationDir.list()
    if (files != null && files.isNotEmpty()) {
        throw IOException("Destination directory is not empty.")
    }
    ZipInputStream(inputStream).use { zipInputStream ->
        var zipEntry: ZipEntry
        while (zipInputStream.nextEntry.also { zipEntry = it } != null) {
            val targetFile = File(destinationDir, zipEntry.name)
            // ...
        }
    }
}

자바

void unzip(final InputStream inputStream, File destinationDir)
        throws IOException {
    if(!destinationDir.isDirectory()) {
       throw IOException("Destination is not a directory.");
    }

    String[] files = destinationDir.list();
    if(files != null && files.length != 0) {
        throw IOException("Destination directory is not empty.");
    }

    try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
        ZipEntry zipEntry;
        while ((zipEntry = zipInputStream.getNextEntry()) != null) {
            final File targetFile =
                    new File(destinationDir, zipEntry.getName());
            // ...
        }
    }
}

리소스