Zip Slip 압축 해제 경로 조작

Zip Slip (압축 해제 경로 조작)

설명

압축 파일 엔트리 이름을 검증하지 않고 파일 경로로 사용하면 ../ 같은 경로를 통해 추출 디렉터리 밖에 파일을 쓸 수 있습니다. 절대 경로와 링크의 처리도 사용 API와 운영체제에 맞게 제한해야 합니다.

잠재적 영향

  • 웹 루트, 설정 파일, 실행 파일을 덮어쓸 수 있습니다.
  • 덮어쓴 파일의 사용 방식과 프로세스 권한에 따라 코드 실행이나 서비스 장애로 이어질 수 있습니다.

해결 방법

  • 새 비공개 추출 디렉터리를 사용하고, 최종 파일 접근이 그 안에 머무르게 하세요.
  • 단일 파일명만 필요한 경우 절대·상위·중첩 경로와 심볼릭 링크 등 비정규 엔트리를 거부하세요.
  • 배타적 생성으로 기존 파일을 덮어쓰지 말고 열기·복사·닫기 오류를 처리하세요.
  • 파일 수와 실제 압축 해제 크기도 제한하세요. 경로 검증만으로 압축 폭탄을 막을 수는 없습니다.

예시

변경 전은 dest와 ZIP 엔트리 file이 준비된 일부입니다. 변경 후는 하위 디렉터리 없이 정규 파일만 추출하며, 같은 계정의 다른 코드가 임시 디렉터리를 조작하지 않는 전제입니다. 추출 크기 제한은 생략했습니다.

변경 전

go
_, _ = os.Create(filepath.Join(dest, file.Name))

변경 후

go
package example

import (
    "archive/zip"
    "fmt"
    "io"
    "os"
    "path/filepath"
)

func extractFlat(r *zip.Reader) (root string, err error) {
    root, err = os.MkdirTemp("", "xeize-unzip-")
    if err != nil {
        return "", err
    }
    createdRoot := root
    defer func() {
        if err != nil {
            _ = os.RemoveAll(createdRoot)
        }
    }()

    for _, file := range r.File {
        if !file.Mode().IsRegular() {
            return "", fmt.Errorf("unsupported archive entry: %q", file.Name)
        }

        name := filepath.Clean(file.Name)
        if name != file.Name || filepath.IsAbs(name) || name == "." || name == ".." ||
            name != filepath.Base(name) {
            return "", fmt.Errorf("invalid archive path: %q", file.Name)
        }

        in, err := file.Open()
        if err != nil {
            return "", err
        }
        out, err := os.OpenFile(filepath.Join(root, name),
            os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
        if err != nil {
            in.Close()
            return "", err
        }

        _, copyErr := io.Copy(out, in)
        closeOutErr := out.Close()
        closeInErr := in.Close()
        if copyErr != nil {
            return "", copyErr
        }
        if closeOutErr != nil {
            return "", closeOutErr
        }
        if closeInErr != nil {
            return "", closeInErr
        }
    }
    return root, nil
}

설명:

  • 변경 전: 결합한 경로가 목적 디렉터리 내부인지 확인하지 않고 파일을 만듭니다.
  • 변경 후: 정규화로 바뀌는 이름과 단일 구성 요소가 아닌 이름을 거부하고 O_EXCL로 중복 생성을 막습니다. 오류가 나면 별도로 보관한 디렉터리 경로로 정리를 시도합니다. 성공 후 디렉터리 정리는 호출자가 담당해야 합니다.

참조