압축을 푼 뒤 파일을 확인하기보다 쓰기 전에 경계를 확인합니다

받은 ZIP을 지정한 폴더에 푼다는 설명만으로 실제 모든 파일이 그 안에 놓인다고 가정하지 마세요. 항목 이름에는 상위 폴더를 가리키는 부분이나 절대 경로, 다른 플랫폼의 경로 표현이 들어갈 수 있습니다. 심볼릭 링크 같은 파일 유형도 일반 문서와 다르게 해석될 수 있습니다. 그래서 외부 아카이브를 처리할 때는 어떤 이름과 유형을 허용할지 먼저 검사하고, 실제 쓰기가 시작되는 조건을 분명히 하는 편이 좋습니다.

이번 실험은 작은 가상 ZIP 여덟 개를 새 임시 폴더에서 만들었습니다. 위험 조건을 넣은 일곱 사례는 사전 검사에서 멈췄고, 정상 사례만 파일 두 개를 썼습니다. 기존 사용자 폴더나 실제 첨부 파일은 열지 않았습니다. 경로 밖에 쓰는 동작을 시도한 실험도 아닙니다. 위험한 이름을 만나면 추출 전에 차단하고, 임시 작업 경계 밖의 파일이 만들어지지 않았는지 확인했습니다. 방어 조건과 실제 실행된 범위를 함께 읽을 수 있도록 결과를 남겼습니다.

Python zipfile 문서도 신뢰하지 않는 아카이브는 먼저 검사하도록 안내합니다. 다만 모든 라이브러리 기능이 똑같이 경로를 처리한다는 뜻은 아닙니다. 어떤 추출 함수는 이름을 정리하려고 하고, ZIP 내부를 경로처럼 다루는 기능에서는 호출자가 직접 검증해야 하는 부분이 있습니다. 이 글의 코드는 외부 도구를 안전하다고 인증하는 대신, 어떤 항목을 거절할지 명시하는 작은 정책을 보여 줍니다.

일곱 차단 사례와 정상 파일 두 개를 따로 만들었습니다

상위 경로를 포함한 이름, 슬래시로 시작하는 절대 경로, C: 드라이브 표현, 역슬래시 경로, 심볼릭 링크 유형, 대소문자만 다른 목적지 두 개, 크기와 압축률 한도를 넘는 작은 반복 자료를 각각 독립 사례로 만들었습니다. 정상 ZIP에는 notes/readme.txt와 data/value.csv를 넣었습니다. CSV의 내용도 Z-31과 숫자 8을 쓰는 가상 자료입니다. 경로와 유형 사례는 각각 별도 ZIP으로 나눴습니다. 크기 예제는 항목·전체 크기·압축률의 여러 한도를 함께 넘도록 만들었으므로 각 한도를 독립적으로 시험한 결과는 아닙니다.

가상 사례확인한 조건작성된 파일
상위·절대·드라이브·역슬래시목적지 경로 정책 위반각 0개
심볼릭 링크일반 파일과 다른 유형0개
Report.txt와 report.txt보수적인 이름 충돌 검사0개
반복된 4,096바이트 내용크기·압축률 한도0개
정상 ZIP허용 경로와 작은 일반 파일2개

여기서 4,096바이트 자료는 실제 시스템에 부담을 주는 대형 압축 파일이 아닙니다. 작은 설정 한도를 넘도록 만들어 분기만 확인했습니다. 압축 폭탄의 실제 자원 소모를 측정하거나 서비스에 올려 시험하지 않았습니다. 다운로드 결과의 한도는 설명용으로 낮게 정한 값이므로, 모든 업무에서 같은 숫자를 쓰면 된다는 권고로 읽지 않아야 합니다.

문자열 앞부분 검사만으로 경로 경계를 정하지 않습니다

사전 검사에서는 ZIP 항목을 POSIX 경로로 보고 절대 경로와 상위 이동 부분을 확인합니다. 역슬래시가 있거나 Windows 드라이브 표현이 있으면 이 예제에서는 보수적으로 거절합니다. 현재 실행한 컴퓨터에서 역슬래시가 평범한 문자처럼 보이더라도 다른 환경에서 다르게 해석될 수 있기 때문입니다. 서로 다른 플랫폼에서 같은 정책을 쓰고 싶다면 입력 이름을 어떤 문법으로 받아들일지 먼저 정해야 합니다. 검출 없이 임의로 구분자를 바꾸면 새로운 이름 충돌도 생길 수 있습니다.

정상으로 분류된 이름도 실제 쓰기 직전에 임시 추출 폴더와 합쳐 절대 경로로 해석하고, 결과가 그 폴더 아래인지 다시 확인합니다. 폴더 이름이 비슷한 다른 경로와 단순 접두사 문자열을 비교하는 방식은 경계를 잘못 판단할 수 있습니다. 코드의 relative_to 검사는 현재 해석된 경로가 지정한 기준 아래인지 확인하기 위한 단계입니다. 하지만 이 검사 한 번이 다른 프로세스가 동시에 경로를 바꾸는 상황까지 보호한다는 뜻은 아닙니다.

이번 실험은 새로 만든 빈 임시 폴더에서 실행하므로 기존 하위 폴더나 링크가 끼어 있지 않습니다. 실제 공유 폴더에서는 검사와 쓰기 사이의 상태 변화, 기존 심볼릭 링크, 권한과 소유자 문제도 확인해야 합니다. 코드가 짧다는 이유로 다중 사용자 환경의 보안 추출기로 그대로 승격하지 않는 것이 중요합니다. 경로 문자열의 판정과 파일시스템의 실제 상태는 연결되지만 서로 다른 검사입니다.

심볼릭 링크와 이름 충돌은 경로 문자열과 별도로 봅니다

심볼릭 링크 사례는 ZIP 메타데이터의 파일 유형 비트를 설정했습니다. 코드는 그 유형을 읽고 추출 전에 거절했습니다. 링크가 가리키는 위치로 실제 파일을 만들지는 않았습니다. 모든 항목을 내용 바이트만 있는 일반 파일로 취급하면 유형 정보가 설명하는 차이를 놓칠 수 있습니다. 업무에서 링크가 꼭 필요한지부터 판단하고, 필요 없다면 허용하지 않는 정책을 명확히 두는 편이 다루기 쉽습니다.

이름 충돌 사례는 Report.txt와 report.txt를 같은 ZIP에 넣었습니다. 실행 환경의 실제 대소문자 구분 방식에 기대지 않고, 비교용 이름을 소문자 계열로 접어 같은 목적지로 판단했습니다. 코드에는 NFC 정규화도 사용하지만 이번 고정 충돌 입력은 대소문자 사례입니다. Unicode의 모든 동등성이나 운영체제별 금지 이름을 실제로 시험한 것은 아닙니다. 보수적인 비교 정책과 실행한 경계 사례를 구분해 표시했습니다.

정상 항목을 쓸 때는 기존 파일을 덮어쓰지 않는 생성 모드를 사용합니다. 이번 폴더는 처음에 비어 있지만, 덮어쓰기 허용을 당연한 전제로 두지 않으려는 선택입니다. 같은 이름의 디렉터리와 파일이 섞이거나 경로의 일부가 이미 파일인 경우 같은 추가 충돌도 실제 추출기에서는 검토해야 합니다. 사전 검사에서 몇 가지 이름을 통과했다는 사실이 모든 가능한 파일 배치를 허용한다는 뜻은 아닙니다.

압축된 크기보다 풀린 크기와 항목 수도 확인합니다

작은 ZIP이라고 풀린 자료도 작다고 보장되지는 않습니다. 실험 코드에는 항목 하나의 크기, 전체 선언 크기, 항목 수와 선언된 압축률에 대한 한도를 뒀습니다. 한 항목은 최대 1,024바이트, 전체는 2,048바이트, 항목 수는 12개, 압축률은 100을 기준으로 설정했습니다. 이것은 재현을 위한 작은 정책 값입니다. 실제 제출 파일을 받는 업무에서는 허용 파일의 성격과 자원 예산을 근거로 별도의 한도를 정해야 합니다.

4,096바이트 반복 자료는 항목·전체 크기 조건을 넘고 높은 압축률도 보였습니다. 따라서 이 입력은 실제 쓰기 전에 차단됐습니다. 다만 ZIP 메타데이터에 적힌 크기를 검사하는 것과, 압축을 푸는 동안 실제로 소모되는 메모리·디스크·시간을 제한하는 것은 다릅니다. 이번 작은 파일 실험은 후자의 강제 제한 장치를 구현하거나 실제 압축 공격을 실행한 결과가 아닙니다. 운영에서는 해제 과정의 누적 바이트와 시간 제한도 별도로 필요할 수 있습니다.

항목 수 한도는 코드에 있지만 이번 여덟 입력은 12개를 초과하는 항목 수 사례를 포함하지 않았습니다. 실행하지 않은 경계까지 통과했다고 표시하지 않습니다. 암호화된 ZIP, 지원하지 않는 압축 방식, 손상된 중앙 디렉터리나 거짓 메타데이터도 별도 검토 대상입니다. OWASP의 파일 업로드 안내를 읽을 때도 압축 파일 한 가지 검사로 업로드 전체가 안전해졌다고 해석하지 않아야 합니다.

검사를 모두 끝낸 뒤에만 쓰고, 성공 결과도 파일별로 확인합니다

각 ZIP에서는 먼저 전체 항목의 검사 결과를 모았습니다. 하나라도 오류가 있으면 그 사례의 쓰기 단계에 들어가지 않습니다. 그래서 위험한 일곱 사례에서 임시 추출 폴더에 남은 파일 수는 모두 0이었습니다. 정상 사례에서는 파일 두 개를 만든 뒤 각각 내용을 읽어 SHA-256를 계산했습니다. 이 값은 실험에서 쓴 내용과 다시 읽은 결과를 대조하는 증거이며, 압축 파일 제공자가 신뢰할 수 있는 사람이라는 인증 정보는 아닙니다.

사전 검사를 통과한 이후에도 실제 디스크 쓰기가 실패할 수 있습니다. 공간 부족이나 권한 오류가 두 번째 파일에서 발생하면 첫 파일만 남는 상황이 생길 수 있습니다. 이번 코드는 새 임시 폴더를 최종적으로 정리하므로 사용자 폴더에 부분 결과를 남기지 않지만, 그 사실이 운영 폴더로의 원자적 게시를 보장하지는 않습니다. 검증용 추출 폴더와 실제 사용하는 위치를 분리하고, 완료를 어떻게 확정할지 설계해야 합니다.

파일을 한꺼번에 이동하는 작업을 이어서 한다면 중간 실패를 주입해 이름과 해시를 복원한 실험도 참고할 수 있습니다. 파일 이름의 정리와 충돌 정책은 이름 변경 전의 사전 검사 가이드에서 따로 다룹니다. 압축 해제, 이름 변환, 최종 배치라는 세 단계의 안전 조건이 서로 같다고 가정하지 않는 편이 좋습니다.

차단 원장은 이름을 고치는 지시서가 아니라 확인할 질문입니다

항목 이름이 거절됐다고 그 이름에서 점 두 개나 슬래시만 지워 다시 추출하면 된다고 결론 내리지 마세요. 서로 다른 항목이 같은 이름으로 합쳐질 수 있고, 원래 경로가 전달하려던 의미도 달라질 수 있습니다. 먼저 자료 제공자에게 의도한 폴더 구조와 허용 파일 유형을 확인하는 편이 낫습니다. 자동 정규화가 필요한 경우에는 원래 이름과 새 이름의 대응표, 충돌 여부와 승인 조건을 별도 원장으로 남길 수 있습니다.

운영 화면에는 아카이브 식별자, 차단한 항목, 사유와 다음 조치를 구분해 표시하세요. 외부에서 받은 이름 자체에 민감한 값이나 제어 문자가 들어갈 수 있으므로 로그 출력 시에도 그대로 실행되거나 화면 구조를 바꾸지 않게 취급해야 합니다. 이번 공개 결과는 새 가상 이름만 사용했으며 실제 사용자 경로를 포함하지 않습니다. 원문 이름을 보존하는 것과 공개 로그에 그대로 보여 주는 것은 다른 결정입니다.

정상 파일의 최종 개수도 목적에 맞게 대조해야 합니다. 라이선스나 문서 파일 같은 일부 항목만 골라 쓰는 경우에는 원래 항목 수와 추출 파일 수가 다를 수 있습니다. 중요한 것은 그 차이가 정책으로 설명되는지입니다. 검사기가 조용히 제외한 파일과 운영자가 의도적으로 제외한 파일을 같은 성공 상태로 합치지 않는 것이 후속 확인에 도움이 됩니다.

아카이브를 받은 상태 그대로 식별할 해시와, 실제로 추출한 각 파일의 내용 해시는 목적이 다릅니다. 공급자가 같은 파일을 다시 보냈는지 확인할 때는 전자가, 추출 결과를 대조할 때는 후자가 필요할 수 있습니다. 이번 실행 결과에는 정상 추출 파일의 해시만 있으므로 그것을 원본 ZIP 전체의 동일성 증거로 사용하지 않습니다.

일곱 차단 성공을 모든 ZIP의 안전성으로 확대하지 않습니다

이 실험은 생성한 작은 ZIP의 경로·유형·일부 크기 조건을 확인했습니다. 악성 코드 검사, 콘텐츠의 진위, 모든 압축 형식의 파서 안전성, 다중 사용자 환경이나 전원 장애를 검증하지 않았습니다. 정상 사례의 두 파일도 업무상 신뢰된 자료가 아니라 고정한 가상 바이트입니다. 해제 경계 안에 놓였다는 사실은 내용이 사실이거나 실행해도 안전하다는 뜻이 아닙니다. 파일을 열고 사용하는 단계에는 다시 별도의 조건이 있습니다.

재현할 때는 위험 사례의 파일 수 0과 정상 사례의 파일 수 2를 먼저 대조하세요. 다음으로 어떤 사유가 어떤 이름을 차단했는지 읽고, 자신의 업무에서 필요한 유형과 경로가 무엇인지 적어 보세요. 기존 폴더에 바로 풀기 전에 작은 가상 입력으로 사전 검사와 실패 정리를 확인하는 것이 이 코드의 용도입니다. 압축 해제 버튼 하나에 모든 검수 의미를 담으려 하지 않는 편이 설명 가능한 작업에 가깝습니다.

입력과 결과를 직접 대조하는 재현 자료

실험 실행일은 2026년 10월 15일이며 이 글의 발행 일정과 구분합니다. Python 3 표준 라이브러리로 가상 자료만 처리했습니다. 아래 파일은 기존 사용자 파일을 인자로 받지 않습니다.

python3 zip-path-traversal-preflight-lab-20261015.py

실행 시각과 런타임은 환경에 따라 달라집니다. 판정 근거는 다운로드 결과의 고정 입력과 각 사례의 실제 출력으로 확인하세요.

규칙을 확인한 공식 원문

아래 원문은 2026년 10월 15일 확인했습니다. 형식과 동작의 설명을 확인한 출처이며, 이 글의 가상 실험이나 실제 업무 성과를 대신 증명하지 않습니다.

  • Python zipfile — 신뢰하지 않는 아카이브의 사전 검사와 ZipInfo 메타데이터 사용을 확인했습니다.
  • Python pathlib — 경로 해석·resolve·상위 경로와 플랫폼별 경로 표현을 확인했습니다.
  • OWASP File Upload Cheat Sheet — 파일명·압축 해제 크기·압축 파일 처리의 방어 범위를 확인했습니다.