클라우드 Mac에서 iOS ZIP 해제 경로와 용량 제한 검증하기

클라우드 Mac에서 iOS ZIP 해제 경로와 용량 제한 검증하기

클라우드 Mac에서 iOS ZIP 해제 경로와 용량 제한 검증하기

사용자가 ZIP 파일을 가져왔고 앱도 성공했다고 알렸는데, 파일은 가져오기 디렉터리 밖에 저장되는 경우가 있습니다. 대개 압축 알고리즘이 아니라 항목 이름을 검증 없이 출력 경로에 이어 붙이는 것이 원인입니다. 이 흐름을 클라우드 Mac에서 검증할 때는 먼저 경로 규칙을 테스트한 뒤 프로젝트의 압축 해제기를 연결해야 합니다. 실제 사용자 자료로 악성 테스트 샘플을 만들거나 테스트가 소스 코드 디렉터리에 파일을 쓰도록 해서는 안 됩니다.

가져오기 허용 범위부터 정의하기

이번 가져오기에서는 일반 파일과 디렉터리만 허용하고, 심볼릭 링크와 알 수 없는 유형은 거부합니다. 가져오기를 실행할 때마다 별도의 임시 저장 디렉터리를 만들고, 모든 검증을 통과한 뒤에만 앱의 데이터 디렉터리로 옮깁니다. 항목 하나라도 실패하면 임시 저장 데이터를 전부 삭제하며, “이미 성공한 부분”도 남기지 않습니다.

구현에 적용할 제한은 세 가지입니다. 항목 수는 최대 천 개, 파일 하나의 크기는 최대 이십 메가바이트, 전체 압축 해제 용량은 최대 백 메가바이트로 설정합니다. 이는 시스템 한계가 아니라 예시 정책이므로 팀에서 서비스 요구에 맞게 조정해야 합니다. 사전 검사에서는 ZIP 디렉터리에 선언된 값을 사용하고, 실제 쓰기 단계에서는 실제 바이트 수를 누적합니다. 두 검사는 서로를 대신할 수 없습니다.

샌드박스가 압축 해제기의 보안 경계를 보장해 주지는 않습니다. 앱 컨테이너 밖에 쓰지 못하더라도 악성 항목이 컨테이너 내부의 데이터베이스나 설정 파일을 덮어쓸 수 있습니다.

실행 가능한 경로 사전 검사 만들기

아래 코드를 zip-path-check.swift로 저장하고 클라우드 Mac에서 xcrun swift zip-path-check.swift를 실행합니다. 이 코드는 항목 이름만 검증하며 ZIP 파일을 읽지는 않습니다. 프로젝트에서는 압축 해제 라이브러리가 제공하는 항목 이름과 유형에 같은 정책을 적용해야 합니다. 명령이 성공했다고 해서 가져오기 전체 과정이 안전해진 것은 아닙니다.

import Foundation

enum PathError: Error {
    case invalid
}

func safeComponents(_ name: String) throws -> [String] {
    guard !name.isEmpty,
          !name.hasPrefix("/"),
          !name.contains("\\"),
          !name.contains(":"),
          !name.unicodeScalars.contains(where: {
              CharacterSet.controlCharacters.contains($0)
          }) else {
        throw PathError.invalid
    }

    let body = name.hasSuffix("/") ? String(name.dropLast()) : name
    let parts = body.split(
        separator: "/",
        omittingEmptySubsequences: false
    ).map(String.init)

    guard !parts.isEmpty,
          parts.allSatisfy({
              !$0.isEmpty && $0 != "." && $0 != ".."
          }) else {
        throw PathError.invalid
    }
    return parts
}

let cases: [(String, Bool)] = [
    ("docs/readme.txt", true),
    ("docs/", true),
    ("report..txt", true),
    ("../outside.txt", false),
    ("docs/../../outside.txt", false),
    ("/tmp/outside.txt", false),
    ("docs//readme.txt", false),
    ("docs/./readme.txt", false),
    ("docs\\..\\outside.txt", false),
    ("C:/outside.txt", false),
    ("docs/\u{0000}bad.txt", false)
]

for (name, expected) in cases {
    let accepted = (try? safeComponents(name)) != nil
    precondition(accepted == expected, "Path policy mismatch")
}
print("Path checks passed")

이 가져오기 규칙은 의도적으로 엄격하게 설계했습니다. 슬래시로 구분된 상대 경로만 허용하며, 디렉터리는 끝에 슬래시 하나를 붙일 수 있습니다. 파일 시스템마다 다르게 해석될 여지를 없애기 위해 콜론과 제어 문자도 거부합니다. 서비스에서 이런 문자를 꼭 사용해야 한다면 모든 검사를 일괄적으로 완화하지 말고, 해당 요구를 명확히 검증하는 샘플을 추가해야 합니다.

링크와 중복 대상 검사하기

경로 검사를 통과한 뒤에도 항목 유형을 확인해야 합니다. 링크를 먼저 압축 해제한 다음, 다음 항목을 쓸 때 그 링크를 따라가게 해서는 안 됩니다. 확장자만 보고 유형을 판단해서도 안 됩니다. 압축 해제 라이브러리가 제공하는 유형과 속성을 읽고, 유형을 확실하게 식별할 수 없다면 바로 거부합니다.

각 대상 경로를 구성 요소 단위로 인덱싱합니다. 완전히 같은 대상뿐 아니라, 파일 docs를 먼저 쓴 뒤 docs/a.txt를 쓰려는 경우처럼 상위 경로와 하위 경로가 충돌하는 경우도 차단해야 합니다. 항목 순서가 반대여도 반드시 실패해야 합니다. 그렇지 않으면 압축 파일 안의 항목 정렬 순서에 따라 실행 결과가 달라집니다.

대소문자만 다르거나 유니코드상 동등한 이름에는 별도 정책이 필요합니다. 가져오는 자료에서 이런 이름을 구분할 필요가 없다면, 경로 구성 요소에 정준 조합 정규화와 케이스 폴딩을 적용한 값을 충돌 검사에 사용할 수 있습니다. 실제 이름은 원래대로 유지합니다. 클라우드 Mac의 현재 볼륨이 대소문자를 처리하는 방식만 보고 iOS의 쓰기 결과를 추정해서는 안 됩니다. 최소한 대소문자 충돌과 유니코드 결합 문자 사례를 각각 준비하고, 대상 시뮬레이터에서 다시 검증해야 합니다.

실제 쓰기로 용량 제한 검증하기

어댑터 계층은 파일에 쓰기 전에 압축 해제된 데이터 블록을 받아야 합니다. 먼저 누적 크기를 검사하고 그다음 파일에 씁니다. 크기를 바로 더하면 정수 오버플로가 발생할 수 있으므로, “남은 허용량으로 이번 블록을 쓸 수 있는가”를 비교합니다. 라이브러리가 데이터를 통째로 메모리에 압축 해제하는 방식만 제공해 중간에 제한을 적용할 수 없다면, 인터페이스를 바꾸거나 입력에 별도의 엄격한 제약을 걸어야 합니다.

각 테스트 압축 파일의 예상 결과를 고정합니다. 정상 디렉터리는 통과하고, 허용 범위를 벗어나는 경로와 중복 대상은 거부하며, 제한을 하나라도 초과하면 거부해야 합니다. 손상된 압축 데이터도 추가해 읽기 오류를 정상적인 종료로 처리하지 않는지 확인합니다. 유효한 압축 파일도 라이브러리가 보고하는 무결성 검사 결과를 확인해야 합니다. 최종 파일이 존재하는지만 봐서는 안 됩니다.

쓰기 실패 후에는 임시 저장 디렉터리가 삭제되었는지, 앱의 데이터 디렉터리가 바뀌지 않았는지, 가져오기 디렉터리 옆에 미리 둔 감시용 파일의 내용이 그대로인지 단언문으로 검증합니다. 이는 함수가 오류를 던졌는지만 확인하는 것보다 유용합니다. 허용 범위 밖에 파일을 이미 쓴 뒤에 오류가 발생했을 수도 있기 때문입니다.

회귀 테스트를 통과 조건으로 연결하고 경계 유지하기

파이프라인에서는 경로 스크립트를 먼저 실행한 다음, 실제 압축 해제기를 호출하는 앱 테스트를 실행합니다. 샘플 식별 번호, 거부 사유, 실제로 쓴 바이트 수는 기록하되 사용자 파일 내용은 기록하지 않습니다. 테스트 디렉터리는 무작위 이름으로 만들고 종료 후 정리합니다. 시스템 임시 디렉터리 자체를 압축 해제 루트로 사용해서는 안 됩니다.

사전 검사와 쓰기 사이에도 디렉터리가 바꿔치기될 수 있습니다. 다른 작업이 변경할 수 없는 앱 전용 임시 저장 디렉터리를 사용하고, 쓰기 단계에서는 링크를 따라가지 않도록 해야 합니다. 여러 작업이 공유하는 데스크톱 스크립트에서 다른 프로세스가 디렉터리를 수정할 수 있다면, 문자열 접두사 검사만으로는 이 위험을 없앨 수 없습니다.

마지막으로 세 가지를 확인합니다. 실패한 가져오기는 일부 데이터도 남기지 않아야 하고, 성공한 가져오기는 예상한 파일만 만들어야 하며, 용량 제한은 실제 바이트 수를 기준으로 적용되어야 합니다. 경로 함수는 첫 번째 관문입니다. 최종 검증은 실제 압축 파일을 쓰는 과정에 대한 단언문까지 통과해야 끝납니다.

자주 묻는 질문

점 두 개가 들어간 파일 이름을 모두 거부하면 충분한가요?

아닙니다. 독립된 .. 경로 구성 요소, 절대 경로, 지원하지 않는 구분자를 거부해야 합니다. 링크, 중복 대상과 쓰기 시점의 디렉터리 경계도 검사하세요. report..txt 자체는 경로 이탈이 아닙니다.

ZIP에 기록된 크기만으로 압축 폭탄을 차단할 수 있나요?

없습니다. 선언된 크기는 사전 검사에 쓰고 실제 쓰기 중에는 해제된 바이트를 누적해야 합니다. 항목별 크기, 전체 크기, 항목 수를 제한하고 초과하면 중단한 뒤 임시 디렉터리를 삭제하세요.

빌드 대기열을 전용 클라우드 Mac에 맡기세요

물리 노드, 모델, 요금제를 선택하면 Xcode 빌드와 자동화 작업을 전용 macOS 환경에서 실행할 수 있습니다. 실제 사용 가능 여부는 콘솔에 표시되는 실시간 상태를 확인하세요.

빌드 환경을 계속 확인하세요

모델 사양과 사용 안내를 비교해 다음 설정을 결정하세요.