보안

안전한 파일 업로드 설계: Presigned URL·Quarantine·악성 파일과 압축 폭탄

AI아키텍트 2026. 7. 30. 12:02

파일 업로드 기능은 단순해 보입니다.

Client가 파일 선택
  → Server 또는 Object Storage에 전송
    → 업무 Pipeline이 파일 처리

하지만 업로드된 파일은 사용자가 보낸 데이터이면서, 동시에 Parser·미리보기·OCR·STT·RAG·변환기 같은 여러 실행 경로를 통과하는 입력입니다.

파일 이름과 Content-Type만 믿고 바로 처리하면 다음 문제가 생길 수 있습니다.

실행 파일을 문서로 위장
확장자와 실제 형식 불일치
Parser 취약점을 노리는 조작된 파일
악성 Macro·Script·Embedded Object
ZIP Bomb·중첩 Archive로 CPU·Memory·Disk 고갈
경로 조작으로 기존 파일 덮어쓰기
같은 Presigned URL 재사용
검사 이후 같은 Object Key 덮어쓰기
검사 실패를 정상으로 오인
비인가 사용자의 Preview·Download
격리 파일이 OCR·RAG·AI Pipeline으로 유입

안전한 Upload Pipeline은 “업로드가 끝났다”를 “업무에서 사용할 수 있다”로 해석하지 않습니다.

Upload Intent 승인
  → 제한된 Upload Ticket 발급
    → Quarantine Storage 직접 업로드
      → 완료 Object의 Version·Size·Checksum 확인
        → 형식·구조·Archive 검증
          → 격리된 Malware Scan·필요 시 CDR
            → 정책 판단
              → Clean Object만 Promotion
                → 업무별 객체 인가 후 사용

이 글은 이전 글의 Upload Ticket 전송 구조를 반복하기보다, 파일이 원격 Storage에 도착한 뒤 안전한 업무 객체가 되기까지의 보안 경계와 상태 계약에 집중합니다.

1. Upload 성공과 사용 가능 상태를 분리한다

Object Storage가 200 OK 또는 204 No Content를 반환했다는 것은 전송과 저장 요청이 성공했다는 뜻입니다.

다음을 의미하지는 않습니다.

  • 요청한 사용자가 해당 업무에 파일을 첨부할 권한이 있음
  • 실제 파일 형식이 허용됨
  • 파일이 손상되지 않음
  • 악성코드가 없음
  • 압축 해제가 안전함
  • Parser와 변환기가 안전하게 처리할 수 있음
  • 다른 사용자가 내려받아도 됨

따라서 Upload 상태와 File 사용 상태를 명시적으로 분리합니다.

INITIATED
  → UPLOADING
    → QUARANTINED
      → VALIDATING
        → SCANNING
          ├─ APPROVED
          ├─ REJECTED
          ├─ REVIEW_REQUIRED
          └─ SCAN_FAILED

APPROVED가 되기 전에는 다음 경로가 파일을 읽지 못해야 합니다.

사용자 Download
문서 Preview
OCR·STT
Thumbnail 생성
Office·PDF 변환
RAG Indexing
LLM·Vision Model
외부 Connector
업무 시스템 Attachment

UI도 uploadCompleted=true 하나로 “첨부 완료”를 표시하지 않습니다.

2. 위협 모델을 파일 내용 밖까지 확장한다

File Upload 위협은 바이러스 하나로 끝나지 않습니다.

영역 대표 위협
권한 다른 Tenant·업무·사용자 대신 업로드
Ticket 탈취·재사용·과도한 만료 시간
Object Key 경로 조작·덮어쓰기·ID 추측
전송 크기 초과·불완전 Multipart·무결성 불일치
형식 확장자·MIME·Magic Byte 불일치·Polyglot
내용 Malware·Macro·Script·Exploit Payload
Archive Entry 폭증·압축 폭탄·중첩·경로 탈출
Parser 취약한 Library·과도한 CPU·Memory·시간
상태 검사 실패를 Clean으로 오인·중복 Event
경합 검사한 Version과 사용하는 Version 불일치
제공 Inline 실행·Content Sniffing·직접 공개 URL
운영 검역 파일 미삭제·민감정보 외부 Scan 전송

OWASP File Upload Cheat Sheet는 허용 확장자, 실제 형식, 생성된 저장 이름, 크기, 인가, 별도 저장소, Antivirus·Sandbox와 CDR을 함께 적용하도록 권고합니다.

어느 하나의 검사도 단독 보안 경계가 아닙니다.

3. Upload Intent를 먼저 승인한다

Presigned URL (사전 서명 URL)을 만들기 전에 업무 권한과 예상 파일 계약을 검증합니다.

{
  "purpose": "meeting_attachment",
  "target": {
    "tenantId": "ten_opaque_id",
    "meetingId": "mtg_opaque_id"
  },
  "file": {
    "originalName": "meeting-audio.wav",
    "declaredMediaType": "audio/wav",
    "declaredSize": 8388608,
    "declaredSha256": "sha256_fixture_digest"
  }
}

Server는 인증 결과에서 다음을 결정합니다.

  • Subject와 Tenant
  • 대상 업무 객체
  • 허용된 Upload Purpose
  • 허용 형식과 최대 크기
  • 저장 위치와 Object Key
  • 필요한 검사 Policy
  • 만료와 재시도 한도

Client가 보낸 tenantId, Object Key, Bucket, Scan Policy를 그대로 신뢰하지 않습니다.

업로드 권한은 “Storage에 쓰기”가 아니라 다음 업무 문장으로 평가해야 합니다.

이 사용자가
이 Tenant의
이 Meeting에
이 목적의 Audio 파일을
이 제한 안에서
첨부할 수 있는가

4. Upload Ticket을 서버가 생성한 Object와 결속한다

Ticket은 넓은 Storage 권한이 아니라 하나의 Upload Intent를 표현합니다.

{
  "uploadId": "upl_opaque_id",
  "subjectId": "usr_opaque_id",
  "tenantId": "ten_opaque_id",
  "purpose": "meeting_attachment",
  "targetObjectId": "mtg_opaque_id",
  "storage": {
    "bucketClass": "quarantine",
    "objectKey": "incoming/ten_opaque_id/upl_opaque_id/source",
    "method": "PUT"
  },
  "constraints": {
    "allowedMediaTypes": [
      "audio/wav"
    ],
    "minimumBytes": 44,
    "maximumBytes": 52428800,
    "requiredChecksum": {
      "algorithm": "SHA256",
      "value": "sha256_fixture_digest"
    }
  },
  "scanPolicyVersion": "upload_security_fixture_v3",
  "expiresAt": "2026-07-29T15:10:00Z"
}

결속해야 할 값은 다음과 같습니다.

  • Upload ID
  • 인증된 Subject·Tenant
  • 업무 Purpose와 대상 Object
  • 서버가 생성한 Quarantine Key
  • 허용 Method
  • 크기 범위
  • 가능한 경우 예상 Checksum
  • 허용 Metadata Header
  • Scan Policy Version
  • 만료 시각

원래 파일명은 표시용 Metadata일 뿐 저장 경로가 아닙니다.

../../report.pdf, 제어 문자, Unicode 혼동 문자와 지나치게 긴 이름이 Storage Key나 File System Path가 되지 않게 합니다.

5. Presigned URL은 비밀번호가 아니라 제한된 Bearer Capability다

Amazon S3 공식 문서에 따르면 Presigned URL은 만료 전까지 여러 번 사용할 수 있습니다.

따라서 “URL을 한 번 발급했으니 자동으로 1회성”이라고 가정하면 안 됩니다.

Presigned URL 자체
  → 특정 Method·Bucket·Key·Header·만료에 대한 제한된 권한

Application Upload Ticket
  → 1회성 Intent·업무 권한·상태·Version·사용 여부

다음 통제를 함께 적용합니다.

  • 짧은 만료 시간
  • HTTPS만 사용
  • URL을 Log·Analytics·Referrer에 남기지 않음
  • 정확한 Object Key와 Method 결속
  • 허용 Header만 서명
  • Storage Policy에서 오래된 Signature 거부
  • Ticket 상태가 INITIATED 또는 허용된 재시도 상태인지 확인
  • 완료 후 Ticket 소비 처리
  • 동일 Key 재업로드를 Version 또는 조건부 쓰기로 구분
  • 유출 의심 시 발급 Credential·Session·Ticket 폐기

AWS S3는 s3:signatureAge 조건으로 Presigned Request의 최대 Signature 나이를 더 짧게 제한할 수 있습니다.

Presigned URL은 인증을 생략하는 공개 URL이 아니라, 소유한 사람이 사용할 수 있는 제한된 Capability입니다.

6. 크기 제한을 세 단계에 둔다

Client가 보낸 declaredSize만 검사하면 실제 전송 크기를 제한하지 못합니다.

크기 Budget은 다음 세 곳에 둡니다.

1. Ticket 발급
   업무 유형별 최대 허용 크기

2. Storage Upload
   POST Policy의 content-length-range 또는 동등한 저장소 제한

3. 완료 검증
   실제 Object Size를 Head·Metadata API로 재확인

Amazon S3 POST Policy는 content-length-range와 정확한 Key·Content-Type 조건을 지원합니다.

PUT 방식에서 동일한 제약을 Storage가 강제할 수 없다면, 고유 Quarantine Key에만 업로드하게 하고 완료 직후 실제 크기를 확인해 초과 객체를 즉시 거부·삭제합니다.

크기 제한은 하나의 전역 숫자가 아니라 Purpose별 정책입니다.

Purpose 고려할 Budget
Profile Image 원본·Pixel 수·Decode Memory
PDF·Office 원본·Page 수·Embedded Object
Audio·Video 원본·재생 시간·Bitrate·Track 수
ZIP 압축 크기·해제 크기·Entry 수·Depth
Dataset 파일 수·총 크기·Row 수·처리 시간

작은 파일도 압축 해제와 Decode 후에는 큰 Resource를 사용할 수 있습니다.

7. Multipart Upload에는 별도 수명과 Budget이 필요하다

대용량 파일은 Multipart Upload를 사용할 수 있습니다.

이 경우 다음 상태가 추가됩니다.

MULTIPART_INITIATED
  → PART_UPLOADING
    → MULTIPART_COMPLETED
      → QUARANTINED

확인할 항목은 다음과 같습니다.

  • 업로드 가능한 Part 수
  • Part별 최소·최대 크기
  • 전체 예상 크기
  • 허용된 Part 번호
  • 완료 요청의 Part 목록과 Checksum
  • 동시 Upload 수
  • 사용자·Tenant별 진행 중 Byte
  • 중단·만료된 Upload 정리

Amazon S3 공식 문서는 완료되지 않은 Multipart Upload가 저장 비용을 계속 발생시킬 수 있으므로 AbortIncompleteMultipartUpload Lifecycle 규칙을 권고합니다.

완료되지 않은 Part를 업무 객체로 세거나 Scan Event로 처리하지 않습니다.

8. Checksum은 무결성 증거이며 악성 여부 증거가 아니다

Checksum은 Client가 의도한 Byte와 Storage에 저장된 Byte가 같은지 확인하는 데 유용합니다.

Expected Checksum
  ↔ Upload Request에 결속
    ↔ Storage가 검증한 Checksum
      ↔ 완료 후 조회한 Object Checksum

Amazon S3 Signature Version 4 Presigned Upload는 여러 Checksum Algorithm을 지원하며, S3는 Single-part와 Multipart Upload의 무결성 검증 기능을 제공합니다.

HTTP 직접 전송에서는 RFC 9530의 Content-Digest 같은 무결성 Field를 Application 계약에 활용할 수 있습니다.

하지만 다음은 서로 다른 판단입니다.

Checksum 일치
  = 전송된 Byte의 무결성 확인

Malware Scan Clean
  = 현재 Engine·Signature·정책에서 위협을 탐지하지 못함

Business Validation 통과
  = 업무가 허용한 형식·구조·내용 조건 충족

공격자가 만든 악성 파일도 정확한 Checksum을 가질 수 있습니다.

또한 Multipart Upload의 ETag를 항상 전체 파일 MD5로 가정하지 않습니다.

9. 업로드 원본은 Quarantine에만 저장한다

업로드 Client가 쓸 수 있는 위치와 업무 Consumer가 읽는 위치를 분리합니다.

Quarantine Storage
  Client: 제한된 Put
  Scanner: Read
  Validator: Read
  Promoter: Read·Copy·Tag
  Business Consumer: Deny
  Public: Deny

Approved Storage
  Client: Deny
  Scanner: 필요 시 Read
  Promoter: 제한된 Write
  Business Consumer: 객체 인가 후 Read
  Public: 기본 Deny

같은 Bucket의 Prefix로 논리 분리할 수도 있지만, 위험과 운영 요구가 높으면 Account·Project·Bucket·Encryption Key까지 분리합니다.

Quarantine Object는 다음 특징을 가집니다.

  • Public ACL 금지
  • 일반 Download API에서 조회 불가
  • CDN·Preview·Search Index 연결 금지
  • 짧은 Retention
  • 별도 Encryption Key와 접근 정책
  • Object Version 추적
  • 자동 Scan Event 대상

격리는 디렉터리 이름이 아니라 실제 IAM·Bucket Policy·Network·Application 권한으로 강제합니다.

10. 완료 Callback에서도 Object를 다시 확인한다

Client의 “업로드 완료” 요청은 Scan 시작을 위한 힌트일 뿐 최종 증거가 아닙니다.

Server는 Storage에서 실제 Object를 조회해 다음을 확인합니다.

{
  "uploadId": "upl_opaque_id",
  "object": {
    "key": "incoming/ten_opaque_id/upl_opaque_id/source",
    "versionId": "ver_opaque_id",
    "size": 8388608,
    "checksumAlgorithm": "SHA256",
    "checksum": "sha256_fixture_digest",
    "lastModified": "2026-07-29T15:04:31Z"
  },
  "ticket": {
    "state": "INITIATED",
    "expiresAt": "2026-07-29T15:10:00Z",
    "maximumBytes": 52428800
  }
}

검증 항목은 다음과 같습니다.

  • Ticket이 존재하고 만료되지 않음
  • 현재 사용자 또는 허용된 비동기 Actor와 연결됨
  • 예상 Bucket·Key와 일치
  • Object Version이 확정됨
  • 실제 크기가 범위 안에 있음
  • Checksum이 필요한 경우 일치
  • Upload 완료 시각이 허용 범위 안에 있음
  • 이미 소비·취소·거부된 Ticket이 아님

성공하면 QUARANTINED로 전이하고, 동일 uploadId + versionId Scan 작업을 멱등하게 생성합니다.

11. 파일 형식은 여러 신호를 교차 검증한다

Content-Type Header는 Client가 설정할 수 있으므로 보안 판정의 단독 근거가 아닙니다.

형식 검증은 다음 신호를 함께 봅니다.

신호 역할 한계
원래 확장자 사용자 의도·표시 쉽게 위조됨
선언 MIME 전송 계약 Client가 조작 가능
Magic Byte·Signature 형식 식별 일부 형식은 공통·가변
Container 구조 ZIP·OOXML·Media 구조 확인 Parser 안전성 필요
실제 Decode·Parse 업무 처리 가능성 확인 취약점·Resource 위험
업무 규칙 Page·Track·Macro 등 허용 범위 형식별 정책 필요
extension = .pdf
declared MIME = application/pdf
detected signature = PDF
parser structure = valid
business policy = encrypted=false, pages<=limit

모든 조건이 일치해야 하는지는 업무별로 정하지만, 불일치를 조용히 보정하지 않습니다.

invoice.pdf.exe, Null Byte, 이중 확장자, Unicode 방향 제어 문자와 대소문자 우회를 정규화한 뒤 검사합니다.

12. Allowlist는 업무에 필요한 최소 형식으로 만든다

“모든 파일을 받은 뒤 검사”보다 “업무에 필요한 형식만 받기”가 먼저입니다.

{
  "policyVersion": "upload_security_fixture_v3",
  "purpose": "knowledge_document",
  "allowedFormats": [
    {
      "format": "PDF",
      "extensions": [
        ".pdf"
      ],
      "mediaTypes": [
        "application/pdf"
      ],
      "maximumBytes": 52428800,
      "encryptedAllowed": false,
      "activeContentAllowed": false
    }
  ],
  "archivesAllowed": false
}

다음 표현은 지나치게 넓습니다.

application/octet-stream 모두 허용
image/* 모두 허용
확장자가 있는 모든 파일 허용
압축 파일 안의 형식은 검사하지 않음
Parser가 열 수 있으면 허용

형식이 늘어날 때는 Parser, Scan, Preview, Download, Retention과 Incident 대응까지 함께 검토합니다.

13. 구조 검증은 안전한 Parser 경계에서 수행한다

Magic Byte가 맞아도 내부 구조가 악의적으로 조작될 수 있습니다.

예를 들어 PDF, Office, Image, Media와 Archive는 서로 다른 검증이 필요합니다.

형식 구조·업무 검증 예
PDF 암호화·Page 수·Embedded File·JavaScript·Form
OOXML Package Entry·Macro·External Relationship·Embedded Object
Image 실제 Decode·Pixel 수·Frame 수·Color Profile
Audio·Video Container·Codec·Track·Duration·Resolution
CSV Encoding·Column 수·Row·Cell 길이·Formula
Archive Entry·Depth·경로·압축률·해제 총량

Parser Worker는 다음 제한을 가집니다.

  • 비특권 사용자
  • Read-only 원본
  • 짧은 수명의 격리 작업 공간
  • CPU·Memory·Disk·Process·시간 제한
  • 기본 Egress 차단
  • 업무 Credential 없음
  • 최신 보안 Patch가 적용된 Library
  • 실패 시 원문과 Stack Trace를 사용자에게 노출하지 않음

검사 과정 자체가 공격 표면이므로 Web Application Process 안에서 무제한으로 Parse하지 않습니다.

14. Polyglot과 Active Content를 별도 판단한다

Polyglot File은 둘 이상의 Parser가 서로 다른 유효 형식으로 해석할 수 있습니다.

파일 Signature 하나만 보고 통과시키면 다른 실행 경로에서 Active Content가 작동할 수 있습니다.

다음 정책을 명확히 합니다.

  • 하나의 Canonical Format만 허용
  • 허용하지 않은 뒤쪽 Payload와 중첩 Container 거부
  • Macro·Script·Embedded Executable 기본 거부
  • HTML·SVG 같은 Active Content의 Inline 제공 금지
  • Office·PDF는 필요 시 CDR (Content Disarm and Reconstruction, 콘텐츠 무해화·재구성) 적용
  • 변환 후 원본과 Sanitized Artifact를 서로 다른 상태로 관리

CDR은 Antivirus를 대체하지 않습니다.

허용된 기능만 남기는 재구성 단계이며, 원본 보존·법적 요구·문서 서명·표현 손실을 함께 검토해야 합니다.

15. Malware Scan은 격리된 실행 환경에서 수행한다

Scanner는 공격자가 만든 파일을 의도적으로 읽는 Component입니다.

따라서 Scanner 자체를 강하게 격리합니다.

Quarantine Object
  → 제한된 Scanner Identity
    → Network Egress가 차단된 Isolated Worker
      → Read-only Input
        → Ephemeral Scratch
          → Engine·Signature Version이 포함된 Result

AWS GuardDuty Malware Protection for S3 공식 문서는 업로드된 Object를 격리된 같은 Region 환경에서 읽고 검사하며, 결과를 Event와 선택적 Object Tag로 제공합니다.

자체 Scanner에서도 다음을 기록합니다.

{
  "scanId": "scn_opaque_id",
  "uploadId": "upl_opaque_id",
  "objectVersionId": "ver_opaque_id",
  "objectChecksum": "sha256_fixture_digest",
  "engine": "managed_or_internal_scanner",
  "engineVersion": "fixture_engine_version",
  "signatureVersion": "fixture_signature_version",
  "policyVersion": "upload_security_fixture_v3",
  "startedAt": "2026-07-29T15:05:00Z",
  "completedAt": "2026-07-29T15:05:12Z",
  "result": "NO_THREATS_FOUND"
}

Engine 이름만 저장하지 말고 어떤 Object Version을 어떤 정책과 Signature로 검사했는지 추적합니다.

16. Scan 결과를 Boolean으로 축소하지 않는다

safe=true/false는 운영에 필요한 실패 의미를 잃습니다.

최소한 다음 결과를 구분합니다.

NO_THREATS_FOUND
THREATS_FOUND
UNSUPPORTED
ACCESS_DENIED
TIMEOUT
ENGINE_ERROR
POLICY_REJECTED
REVIEW_REQUIRED

AWS GuardDuty도 NO_THREATS_FOUND, THREATS_FOUND, UNSUPPORTED, ACCESS_DENIED, FAILED 같은 결과를 구분합니다.

정책 예시는 다음과 같습니다.

결과 기본 처리
NO_THREATS_FOUND 다른 검증도 통과한 경우에만 Promotion 후보
THREATS_FOUND 즉시 차단·격리 유지·보안 Event
UNSUPPORTED 허용하지 않거나 별도 검토
ACCESS_DENIED Scanner 권한 장애·Fail Closed
TIMEOUT 더 작은 Budget·별도 Sandbox 또는 거부
ENGINE_ERROR Retry Budget 내 재시도 후 운영 Alert
REVIEW_REQUIRED 제한된 Reviewer Workflow

NO_THREATS_FOUND는 현재 Scanner가 위협을 탐지하지 못했다는 의미이지, 파일이 절대 안전하다는 증명이 아닙니다.

17. 검사 실패는 Clean이 아니다

가장 위험한 구현 중 하나는 다음과 같습니다.

try:
  scan(file)
catch:
  continue_processing()

Scanner Timeout, Queue 지연, 권한 오류, 암호화 형식, 지원하지 않는 압축 방식과 Engine 장애를 모두 “문제 없음”으로 바꾸기 때문입니다.

Security Decision은 명시적인 Blocking Gate입니다.

{
  "formatValidation": "PASSED",
  "archiveValidation": "NOT_APPLICABLE",
  "malwareScan": "ENGINE_ERROR",
  "contentDisarm": "NOT_REQUIRED",
  "decision": "SCAN_FAILED",
  "promotionAllowed": false,
  "reasonCodes": [
    "MALWARE_ENGINE_UNAVAILABLE"
  ]
}

가용성을 위해 Fail Open이 필요한 특수 업무가 있다면 위험, 데이터 분류, 대체 통제, 승인자와 만료를 별도 정책으로 관리합니다.

일반 사용자 업로드를 자동으로 OCR·RAG·공유하는 경로에서는 기본적으로 Fail Closed가 안전합니다.

18. 외부 Reputation·Scan API의 데이터 유출을 검토한다

파일 Hash나 원본을 외부 서비스에 보내는 행위도 데이터 처리입니다.

다음을 먼저 확인합니다.

  • 원본 File 전송 여부
  • Hash만 전송하는지
  • 보관 기간과 학습·분석 이용 조건
  • 처리 Region
  • Tenant·계약상 외부 제공 허용 여부
  • 개인정보·기밀정보 포함 가능성
  • 암호화와 Access Log
  • 삭제 요청과 사고 통지

OWASP도 공개 Virus Scan 서비스 사용 시 정보 유출 가능성을 주의하도록 설명합니다.

민감한 기업 문서를 사용자의 동의와 처리 계약 없이 공개 Reputation Service에 업로드하지 않습니다.

19. 압축 파일은 파일 하나가 아니라 실행 계획으로 취급한다

Archive는 작은 입력이 많은 파일과 큰 출력으로 변환되는 Container입니다.

다음 값을 사전에 읽거나 Streaming 중 누적합니다.

{
  "archivePolicy": {
    "maximumArchiveBytes": 52428800,
    "maximumEntryCount": 1000,
    "maximumEntryBytes": 104857600,
    "maximumTotalUncompressedBytes": 536870912,
    "maximumCompressionRatio": 100,
    "maximumNestedDepth": 2,
    "maximumPathLength": 240,
    "maximumProcessingSeconds": 30,
    "encryptedEntriesAllowed": false,
    "symbolicLinksAllowed": false
  }
}

숫자는 설명용 예시이며 업무와 처리 환경에 맞게 측정해 정합니다.

검사 항목은 다음과 같습니다.

  • Entry 수
  • Entry별 압축·해제 크기
  • 전체 예상·실제 해제 크기
  • 압축률
  • 중첩 Archive 깊이
  • 암호화 Entry
  • 지원하지 않는 압축 방식
  • CPU·Memory·Disk·시간
  • 중복 정규화 경로
  • Symbolic Link·Hard Link·Device Entry

Header의 예상 크기만 믿지 않고 실제 Streaming 해제 Byte를 계속 계산합니다.

20. 압축 폭탄은 해제 도중에도 차단한다

ZIP Bomb은 작은 압축 파일이 매우 큰 출력으로 확장돼 Resource를 고갈시키는 공격입니다.

OWASP는 ZIP Bomb을 File Upload 위협으로 명시합니다.

Apache Commons Compress는 압축·해제 Byte 통계를 제공하는 InputStreamStatistics를 통해 비정상 압축률을 감지할 수 있다고 설명합니다.

안전한 Extractor는 다음 조건 중 하나라도 넘으면 즉시 중단합니다.

entryCount > maxEntries
currentEntryBytes > maxEntryBytes
totalUncompressedBytes > maxTotalBytes
uncompressedBytes / max(compressedBytes, 1) > maxRatio
nestedDepth > maxDepth
elapsedTime > maxTime
scratchDiskBytes > maxDisk
memoryBytes > maxMemory

중요한 점은 “압축을 다 푼 다음 크기를 확인”하지 않는 것입니다.

제한된 Scratch Volume에 Streaming으로 쓰면서 Budget을 초과하는 순간 Worker를 종료하고 생성된 파일을 모두 폐기합니다.

21. Archive 경로는 정규화 후 격리 Root 안에 있어야 한다

Archive Entry 이름에는 다음 값이 들어갈 수 있습니다.

../../outside.txt
/absolute/path.txt
C:\absolute\path.txt
safe/../../../outside.txt
normal-name + Unicode 혼동 문자
같은 정규화 결과를 가진 중복 경로
Symbolic Link가 가리키는 외부 경로

각 Entry는 다음 순서로 처리합니다.

원본 Entry Name
  → Encoding·Separator 정규화
    → 절대 경로·Drive·UNC 거부
      → "."·".." Segment 해석
        → 최종 Canonical Path 계산
          → Extract Root 하위인지 확인
            → Link·Device·중복 정책 검사
              → 제한된 권한으로 생성

String Prefix 비교만 사용하지 말고 File System의 Canonical Path 기준으로 Root 경계를 확인합니다.

Archive 안의 원래 권한 Bit를 그대로 복원해 실행 파일이나 특수 파일을 만들지 않습니다.

22. 검사한 Object Version과 사용하는 Version을 결속한다

검사가 끝난 뒤 같은 Key가 덮어써지면 Clean 판정과 실제 Byte가 달라질 수 있습니다.

Version A 업로드
  → Version A Scan Clean
    → 같은 Key에 Version B 업로드
      → "Key가 Clean"이라는 상태로 Version B 사용

이 경쟁 조건을 막으려면 모든 단계가 다음 Identity를 사용해야 합니다.

bucket
objectKey
objectVersionId
checksum
size
uploadId
scanPolicyVersion

latest Object를 다시 읽지 않습니다.

Scan Job, Result, Promotion, 업무 Attachment와 Audit Event가 동일한 objectVersionId + checksum을 가리켜야 합니다.

Versioning이 없는 Storage라면 서버가 생성한 고유 Key, 조건부 쓰기와 완료 후 불변 정책으로 같은 효과를 만듭니다.

23. Event는 중복·역순·누락을 전제로 처리한다

Object Storage Event는 하나의 정확히 한 번 전달되는 Transaction Log가 아닙니다.

Amazon S3 Event Notification은 At-least-once Delivery (최소 한 번 전달)이며 순서를 보장하지 않고 중복될 수 있습니다.

따라서 다음 Event 처리는 멱등해야 합니다.

ObjectCreated Version A
ObjectCreated Version A 중복
ScanCompleted Version A
ObjectCreated Version B
ScanCompleted Version A 지연 도착
ObjectDeleted Version A

멱등 Key 예시는 다음과 같습니다.

validation:{bucket}:{key}:{versionId}:{policyVersion}
scan:{bucket}:{key}:{versionId}:{engineVersion}:{signatureVersion}
promotion:{uploadId}:{versionId}:{checksum}:{policyVersion}

Event가 상태를 임의로 덮어쓰지 않게 Generation·Revision 조건을 둡니다.

주기적인 Reconciliation (대사) 작업으로 다음을 찾습니다.

  • Object는 있지만 Upload Record가 없음
  • QUARANTINED에서 너무 오래 머묾
  • Scan Result는 있지만 상태 반영이 안 됨
  • APPROVED인데 Approved Object가 없음
  • Rejected Object가 Retention을 초과함

24. Promotion은 새 신뢰 경계로의 조건부 복사다

검사를 통과했다고 Quarantine Object의 ACL만 넓히는 방식은 경계를 흐리게 할 수 있습니다.

안전한 Promotion (승격)은 승인된 Byte를 Approved Storage의 새 Key·Version으로 복사하고 업무 객체와 연결합니다.

Quarantine Object Version
  + Validation Evidence
  + Malware Scan Evidence
  + Policy Decision
    → Conditional Promotion
      → Approved Object Version
        → Business Attachment 활성화

Promotion 조건은 다음과 같습니다.

{
  "uploadId": "upl_opaque_id",
  "source": {
    "objectVersionId": "ver_opaque_id",
    "checksum": "sha256_fixture_digest"
  },
  "evidence": {
    "formatValidation": "PASSED",
    "archiveValidation": "NOT_APPLICABLE",
    "malwareScan": "NO_THREATS_FOUND",
    "scanId": "scn_opaque_id"
  },
  "policyVersion": "upload_security_fixture_v3",
  "decision": "APPROVED",
  "destination": {
    "objectKey": "approved/ten_opaque_id/fil_opaque_id/content"
  }
}

Promoter는 다음을 다시 확인합니다.

  • 모든 Blocking Gate 통과
  • Evidence가 같은 Source Version·Checksum에 결속
  • 현재 Upload·업무 객체 권한과 상태
  • 이미 같은 Promotion이 완료됐는지
  • 목적지 Key가 서버 생성 값인지
  • Encryption·Retention·Classification Policy

복사 후 Destination Checksum을 확인하고 조건부 Transaction으로 Attachment를 활성화합니다.

25. Approved 파일도 객체 단위 인가 후 제공한다

Malware Scan 통과는 모든 사용자의 읽기 권한을 부여하지 않습니다.

Download·Preview 시 다음을 재검증합니다.

  • 현재 Subject와 Tenant
  • 업무 Object 접근 권한
  • File Classification
  • Attachment 상태
  • 삭제·격리·철회 여부
  • 현재 정책과 보존 상태
  • 필요한 승인·목적

Public Bucket URL 대신 Application Handler 또는 짧은 수명의 제한된 Download URL을 사용합니다.

Browser 제공 시 형식과 업무에 따라 다음을 고려합니다.

Content-Type: 검증된 실제 형식
Content-Disposition: attachment; filename="sanitized-name.pdf"
X-Content-Type-Options: nosniff
Content-Security-Policy: sandbox 또는 별도 Origin

HTML, SVG, PDF, Office처럼 Active Content 가능성이 있는 형식은 주 Application Origin에서 Inline으로 실행하지 않습니다.

원래 파일명은 Header Injection과 Unicode 혼동을 막도록 정제합니다.

26. 재검사와 정책 변경을 고려한다

오늘 Clean이었던 파일이 새 Malware Signature 또는 Parser 취약점 공개 후에도 영원히 안전하다고 볼 수는 없습니다.

다음 조건에서 Re-scan을 고려합니다.

  • Malware Engine·Signature 주요 Update
  • 새로운 고위험 취약점·Indicator
  • 장기 보관 후 다시 사용
  • 외부 공유·Export 전
  • 보안 사고 조사
  • Scan Policy 강화
  • 기존 UNSUPPORTED 형식 지원 추가
Approved Object
  → RESCAN_REQUIRED
    → 사용 제한 또는 기존 권한 유지 정책
      → Re-scan
        ├─ APPROVED 유지
        └─ REVOKED·QUARANTINED

재검사 중 기존 사용을 허용할지는 데이터 분류와 위협 수준에 따라 정합니다.

새 위협이 발견되면 Download·Preview·Pipeline 사용을 즉시 차단하고 파생 Thumbnail·Text·Embedding·Cache도 추적해 철회합니다.

27. 삭제와 Retention을 상태별로 설계한다

Quarantine Storage가 악성·민감 파일의 영구 보관소가 되면 안 됩니다.

상태 Retention 원칙
INITIATED·만료 Upload Record와 미완료 Part 정리
QUARANTINED Scan SLO를 넘으면 Alert·정리
REJECTED 짧은 보존 후 삭제, 필요한 최소 증거만 유지
THREATS_FOUND Incident 정책에 따른 격리·제한·삭제
SCAN_FAILED Retry·운영 조사 후 정리
APPROVED 업무·법규·계약 Retention 적용
DELETED 원본·복사본·파생물·Cache 삭제 전파

악성 파일 원문을 보안팀이 조사 목적으로 보존해야 한다면 일반 Quarantine과 분리하고 더 강한 접근 통제와 Legal Hold 절차를 적용합니다.

Audit Log에는 원문과 Presigned URL을 저장하지 않고 Object ID, Version, Checksum, Decision, Reason Code와 Actor를 기록합니다.

28. 운영 Event와 Metric을 구조화한다

파일 내용 전체를 Log에 남기지 않아도 Pipeline을 운영할 수 있어야 합니다.

{
  "eventType": "file_upload_security_decided",
  "uploadId": "upl_opaque_id",
  "fileId": "fil_opaque_id",
  "subjectId": "usr_opaque_id",
  "tenantId": "ten_opaque_id",
  "purpose": "meeting_attachment",
  "sourceObjectVersionId": "ver_opaque_id",
  "sourceChecksum": "sha256_fixture_digest",
  "declaredFormat": "audio/wav",
  "detectedFormat": "audio/wav",
  "actualBytes": 8388608,
  "scanId": "scn_opaque_id",
  "scanResult": "NO_THREATS_FOUND",
  "policyVersion": "upload_security_fixture_v3",
  "decision": "APPROVED",
  "reasonCodes": [],
  "occurredAt": "2026-07-29T15:05:14Z"
}

운영 Metric 예시는 다음과 같습니다.

  • Upload Intent 승인·거부율
  • Presigned URL 발급 후 완료율
  • 만료·재사용·크기 초과율
  • Quarantine 체류 시간
  • Validation·Scan 단계별 P50·P95·P99
  • 형식 불일치·Parser 실패율
  • Malware·Unsupported·Timeout·Engine Error 수
  • Archive 압축률·Entry·해제 Byte 분포
  • Promotion 성공·중복·실패율
  • Orphan Object와 미완료 Multipart Byte
  • 사용자·Tenant·Purpose별 Upload Byte와 비용
  • 승인 후 재격리·철회 수

보안 Alert는 반복 Ticket 발급, 비정상 압축률, 확장자·형식 불일치, Malware 탐지와 대량 실패를 연결해 봅니다.

29. Negative Test를 Release Gate로 사용한다

정상 PDF 하나를 올려 성공하는 테스트만으로 Upload 보안을 검증할 수 없습니다.

시험 기대 결과
다른 Tenant ID로 Ticket 요청 Server Context로 거부
Object Key 변조 Signature 또는 완료 검증 실패
만료된 Presigned URL Storage·Application 모두 거부
같은 URL 재사용 새 Version을 업무에 연결하지 않음·Alert
허용 크기 초과 Storage 또는 완료 검증에서 거부
선언 Checksum 불일치 Upload·완료 검증 실패
.pdf.exe·이중 확장자 정규화 후 거부
PDF 확장자에 실행 파일 Signature·Parser 검증 실패
Polyglot File 정책상 거부·Review
Malware Test Fixture THREATS_FOUND·Promotion 금지
Scanner Timeout Clean 처리 금지·SCAN_FAILED
Scanner 권한 제거 ACCESS_DENIED·Fail Closed
Scan Result 중복 Event Promotion 한 번만 실행
오래된 Clean Event 지연 도착 새 Version 상태를 덮어쓰지 않음
같은 Key를 Scan 후 덮어쓰기 다른 Version으로 차단
ZIP Entry 수 초과 Streaming 중단·Scratch 삭제
압축률 초과 Decompression Bomb 판정
중첩 Archive Depth 초과 거부
../·절대 경로 Entry Extract Root 밖 생성 0건
Symbolic Link Entry 거부
Approved 전 Preview 요청 존재·내용 비노출
다른 사용자 Download 객체 인가 실패
미완료 Multipart 방치 Lifecycle·Reconciliation으로 정리
Rejected Object Retention 초과 자동 삭제·증거만 유지

Malware Test Fixture는 Production 사용자 데이터와 분리하고, 조직이 승인한 안전한 테스트 절차를 사용합니다.

30. 운영 전 체크리스트

Intent·Ticket

  • Upload 전에 사용자·Tenant·Purpose·대상 객체 권한을 확인한다.
  • Bucket·Key·Method·크기·형식·만료를 Server가 결정한다.
  • 원래 파일명을 Storage Path로 사용하지 않는다.
  • Presigned URL을 Secret과 같은 방식으로 Log·분석 도구에서 제외한다.
  • Presigned URL이 만료 전 재사용될 수 있음을 전제로 Application Ticket을 1회성으로 관리한다.
  • Signature Age와 Credential 수명을 짧게 제한한다.

전송·Storage

  • Storage 또는 완료 검증에서 실제 크기를 강제한다.
  • Checksum을 Upload Request와 완료 Object에 결속한다.
  • Multipart Part·동시성·전체 Byte·만료 Budget을 둔다.
  • 미완료 Multipart Upload Lifecycle 정리를 설정한다.
  • 업로드 원본은 Public·Business Consumer가 읽지 못하는 Quarantine에만 저장한다.
  • Quarantine과 Approved Storage의 Identity·권한을 분리한다.

형식·내용 검증

  • 확장자·선언 MIME·Signature·구조·업무 규칙을 교차 검증한다.
  • Purpose별 최소 Allowlist와 최대 크기를 관리한다.
  • Parser를 비특권·무자격 증명·Egress 차단 Worker에서 실행한다.
  • Macro·Script·Embedded Object·Polyglot 정책이 있다.
  • 필요한 형식에 Antivirus·Sandbox·CDR을 조합한다.
  • Scanner Engine·Signature·Policy Version을 결과에 기록한다.
  • Unsupported·Timeout·Access Denied·Engine Error를 Clean으로 처리하지 않는다.
  • 외부 Scan Service로 파일·Hash를 보낼 때 데이터 처리 조건을 검토한다.

Archive

  • Entry 수·개별·전체 해제 크기·압축률·중첩 깊이를 제한한다.
  • CPU·Memory·Disk·시간 Budget을 적용한다.
  • Header뿐 아니라 Streaming 중 실제 해제 Byte를 계산한다.
  • 절대 경로·상위 경로·중복 Canonical Path를 거부한다.
  • Symbolic Link·Hard Link·Device Entry와 원래 권한 복원을 제한한다.
  • 암호화·지원하지 않는 압축 방식을 명시적으로 처리한다.
  • 실패 시 Scratch와 부분 산출물을 모두 폐기한다.

상태·Promotion·제공

  • Upload 완료와 APPROVED 상태를 분리한다.
  • Scan·Decision·Promotion을 동일 Object Version·Checksum에 결속한다.
  • 중복·역순 Event와 늦은 Result를 멱등하게 처리한다.
  • 모든 Blocking Gate를 통과한 객체만 Approved Storage로 조건부 Promotion한다.
  • Promotion 후 Destination Checksum과 업무 Attachment Transaction을 확인한다.
  • Download·Preview에서 현재 객체 권한을 재검증한다.
  • Active Content는 별도 Origin·Attachment·nosniff 정책으로 제공한다.
  • 재검사·철회 시 파생 Text·Thumbnail·Embedding·Cache까지 차단한다.

운영·검증

  • 상태별 Retention과 삭제 정책이 있다.
  • Quarantine SLO·Orphan Object·미완료 Multipart를 Reconciliation한다.
  • 원문·Presigned URL·민감 Metadata를 Log에 남기지 않는다.
  • Upload·Validation·Scan·Promotion Event를 Upload ID와 Decision ID로 연결한다.
  • 크기·형식·Malware·Archive·경합·권한 Negative Test를 Release Gate로 실행한다.
  • Scanner·Parser·Queue·Storage 장애 시 Fail Closed 동작을 검증한다.

마무리

안전한 File Upload 흐름은 다음과 같이 정리할 수 있습니다.

인증된 Upload Intent
  → 제한된 1회성 Application Ticket
    → 짧은 수명의 Presigned URL
      → Quarantine Storage
        → 실제 Size·Version·Checksum 확인
          → 확장자·MIME·Signature·구조 검증
            → 제한된 Archive 해제
              → 격리된 Malware Scan·필요 시 CDR
                → 명시적 Security Decision
                  → 같은 Version·Checksum만 Promotion
                    → 업무 객체 인가 후 Preview·Download
                      → 재검사·철회·삭제 전파

핵심 원칙은 다음과 같습니다.

  1. Upload 성공을 사용 가능 상태로 해석하지 않습니다.
  2. Presigned URL을 제한된 Bearer Capability로 취급하고 Application에서 1회성 Intent를 관리합니다.
  3. Client가 쓸 수 있는 Quarantine과 업무가 읽는 Approved Storage를 분리합니다.
  4. 파일명과 Content-Type 대신 Signature·구조·업무 규칙을 함께 검증합니다.
  5. Scanner와 Parser를 공격 표면으로 보고 CPU·Memory·Disk·Network·Credential을 제한합니다.
  6. Checksum, Malware Scan과 Business Validation을 서로 다른 증거로 관리합니다.
  7. 압축 파일은 Entry·해제 Byte·압축률·깊이·경로를 Streaming 중 제한합니다.
  8. 검사한 Object Version·Checksum과 Promotion·사용 Object를 끝까지 결속합니다.
  9. Scanner 실패·Timeout·Unsupported를 Clean으로 바꾸지 않습니다.
  10. Negative Test와 Reconciliation으로 정상 경로 밖의 실패를 계속 검증합니다.

파일 업로드 보안의 목표는 “악성 파일을 잘 찾는 Scanner 하나”가 아닙니다.

검증되지 않은 Byte가 사용자, Parser, 업무 Pipeline과 AI Context에 도달하지 못하고, 승인된 정확한 Object Version만 최소 권한으로 사용됐다는 증거를 만드는 것입니다.

다음 글에서는 AI Agent의 정책 결정, Approval, Tool Side Effect와 보안 Event를 하나의 Trace로 연결하고, 개인정보 Masking·보존·탐지·사고 대응까지 포함하는 감사 로그 설계를 살펴보겠습니다.

참고 자료


이 글은 2026년 7월 29일 기준 OWASP, AWS, Apache Commons Compress, IETF와 NIST의 공식 공개 자료 및 공개 가능한 엔터프라이즈 File Upload 보안 설계 경험을 바탕으로 작성했습니다. 예시 ID, Domain, Object Key, 파일명, 크기, Hash, Version, 수명과 정책은 설명용 Fixture이며 실제 고객·Tenant·계정·Storage·내부 시스템 정보가 아닙니다. 실제 적용 시 Object Storage의 Presigned Request·Versioning·Checksum·Event 의미, 지원 File Format, Parser와 Malware Scanner의 한계, 데이터 분류, 외부 처리 계약, 보존·삭제 의무, 관련 법규와 업무별 Resource Budget을 검토하고 악성 파일·압축 폭탄·경로 조작·중복 Event·Version 경합·권한 우회 Negative Test로 검증해야 합니다.