압축 파일 경로 순회

OWASP 카테고리: MASVS-STORAGE: 저장소

개요

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)
            // ...
        }
    }
}

Java

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);
        …
    }
  }
}

리소스

  • 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
  • 경로 순회