## 개요 이 문서는 **인증 사용자**와 **공유 링크 사용자** 모두를 지원하는 파일 다운로드 시스템의 전체 흐름을 정의한다. 핵심 구조는 다음과 같다: > **권한 검증 → 다운로드 가능 상태 사전 검증 → 다운로드 세션 발급 → 최종 경로 검증 및 스트리밍** --- ## 전체 흐름 (8단계) ![[download_flowchart_part1.svg]] ![[download_flowchart_part2.svg]] --- ## 단계별 상세 ### 1단계: 사용자 인증 / 공유 링크 접근 **목적:** 요청자의 신원을 확인하고 내부 컨텍스트로 정규화한다. | 접근 경로 | 인증 방식 | 정규화 결과 | |-----------|-----------|-------------| | 인증 사용자 | `Authorization` 헤더의 JWT Bearer 토큰 | `subjectType=user`, `subjectId={userId}` | | 공유 링크 사용자 | 쿼리 파라미터 또는 별도 엔드포인트의 `share_token` | `subjectType=share_link`, `subjectId={shareLinkId}` | **에러 처리:** | 상황 | 응답 | |------|------| | 인증 토큰 유효하지 않음 / 만료 | `401 Unauthorized` | | 공유 토큰 유효하지 않음 | `404 Not Found` | --- ### 2단계: 파일 권한 / 공유 상태 검증 **목적:** DB 조회를 통해 요청자가 해당 파일에 접근할 수 있는지 확인한다. **검증 항목:** | 접근 유형 | 검증 내용 | | --------- | ---------------------------------------- | | 인증 사용자 | 파일 소유자 여부, 공유 대상 여부, 조직/프로젝트/폴더 단위 접근 권한 | | 공유 링크 사용자 | 링크 활성화 여부, 만료 여부, 비밀번호 검증, 다운로드 허용 여부 | **핵심 원칙:** 파일 존재 여부 노출을 최소화하기 위해 아래 경우는 **모두 `404 Not Found`** 로 처리한다. - 파일이 실제로 존재하지 않음 - 요청자에게 접근 권한이 없음 - 공유 링크가 파일에 매핑되지 않음 - 비활성화된 공유 링크로 접근 시도 > 외부 클라이언트 관점에서 **"접근 불가"와 "존재하지 않음"을 구분할 수 없다.** --- ### 3단계: 파일 메타데이터 및 다운로드 가능 상태 검증 **목적:** 다운로드가 불가능한 파일에 대해 세션을 발급하지 않기 위한 **사전 검증**이다. **검증 항목:** - 파일 메타데이터 존재 여부 - `storage_key` 존재 여부 - 삭제 / 손상 / 비활성 / 격리 상태 여부 - 다운로드 정책상 차단 대상 여부 - (가능 시) 실제 스토리지 객체 존재 여부 **실패 시:** `404 Not Found` > ⚠️ 이 단계는 사전 검증이다. 세션 발급 후 실제 스트리밍 시점까지 파일 상태가 변할 수 있으므로, **최종 검증은 6단계에서 다시 수행**한다. --- ### 4단계: 다운로드 세션 토큰 발급 **목적:** 실제 파일 스트리밍에 사용할 세션 토큰을 발급한다. **세션 토큰 속성:** | 항목 | 값 | |------|-----| | TTL | 약 5분 | | 포함 정보 | 파일 ID, 요청자 식별자(`subjectType`, `subjectId`), 세션 ID, 만료 시각 | | 보호 방식 | HMAC 또는 JWT 서명, 필요 시 Redis/DB에 메타데이터 저장 | **세션이 지원하는 요청 유형:** - 브라우저의 동일 다운로드 재요청 - `Range` 기반 이어받기(resume) - 네트워크 끊김 후 재시도 - 일부 브라우저/다운로드 매니저의 병렬 Range 요청 **응답 예시:** ```json { "downloadUrl": "/files/stream?sessionToken={download_session_token}", "expiresAt": "2026-03-17T12:00:00Z" } ``` **리스크 및 대응:** | 리스크 | 대응 | |--------|------| | TTL이 너무 길면 URL 탈취 악용 가능 | TTL 5분 내외로 제한 | | TTL이 너무 짧으면 대용량 파일 재요청 실패 | 세션 TTL 내 반복 요청 허용 | | 토큰 유출 | 파일 ID + 요청자 식별자에 바인딩, 선택적 IP/UA 바인딩 | **추가 보안:** - 다운로드 세션은 **발급 시점 권한**을 기준으로 TTL 동안 유효 - Access log 마스킹 - `Cache-Control: private, no-store` --- ### 5단계: 세션 토큰으로 파일 스트림 요청 **클라이언트 요청 예시:** ```http GET /files/stream?sessionToken={download_session_token} Range: bytes=0-1048575 ``` **처리 규칙:** | 상황 | 처리 | |------|------| | 세션 토큰 만료 / 위변조 | `401 Unauthorized` | | 세션이 가리키는 파일과 요청 대상 불일치 | `404 Not Found` | | 세션 TTL 내 반복 GET / Range / resume | ✅ 허용 | | Multi-range 요청 | ❌ 미지원 (정책에 따라 `416` 또는 전체 응답) | **설계 원칙:** > 세션 자체는 짧게 유지하되, 세션이 살아있는 동안은 브라우저 다운로드 모델을 수용한다. **"1회용 토큰 + 즉시 무효화" 방식을 사용하지 않는 이유:** 브라우저는 다운로드 중 동일 URL로 재요청, Range resume, 병렬 요청, 프록시 중복 요청 등을 발생시킬 수 있어 충돌한다. --- ### 6단계: `storage_key` → 실제 파일 경로 resolve 및 최종 검증 **목적:** 스트림을 열기 직전에 실제 파일 경로를 확정하고 최종 검증을 수행한다. **처리 절차:** 1. 키-경로 맵(DB 또는 설정) 조회 → 절대 경로 획득 2. `realpath()` 등으로 경로 정규화 3. 정규화된 경로가 허용된 루트 디렉터리 내부인지 검증 (예: `/data/uploads/`) 4. 실제 파일 존재 여부 확인 5. 파일 open 가능 여부 확인 **보안 요구사항:** - Path traversal 방어 필수 (`../`, 심볼릭 링크 우회, 상대 경로 등 차단) - 허용 루트 외부로 벗어나면 즉시 실패 처리 **에러 처리:** | 상황 | 응답 | 내부 처리 | |------|------|-----------| | 실제 파일 미존재 | `404 Not Found` | - | | 메타데이터 존재하나 스토리지 파일 누락 | `404 Not Found` | 경고 로그 / 장애 알림 | --- ### 7단계: 파일 스트림 응답 **기본 응답 헤더:** | 헤더 | 값 | |------|-----| | `Content-Type` | 서버가 결정한 안전한 MIME 타입 | | `Content-Disposition` | `attachment; filename="..."; filename*=UTF-8''...` | | `Accept-Ranges` | `bytes` | | `Content-Length` | 전체 또는 부분 응답 크기 | | `ETag` | 파일 버전 식별자 | | `Last-Modified` | 파일 최종 수정 시각 (선택) | **Range 처리:** | 조건 | 응답 | |------|------| | `Range` 헤더 없음 | `200 OK` (전체 파일) | | `Range` 헤더 있음 | `206 Partial Content` (부분 응답) | | 범위 초과 | `416 Range Not Satisfiable` | | `If-Range` 일치 | 부분 응답 | | `If-Range` 불일치 | 전체 파일 재전송 또는 정책에 따라 처리 | **MIME 타입 처리 원칙:** 1. 서버 측 MIME 판별 사용 (클라이언트 업로드 값 불신) 2. 저장된 메타데이터와 확장자는 보조적으로 사용 3. 판별 불확실 시 `application/octet-stream` 4. XSS 가능 타입이라도 `Content-Disposition: attachment`로 강제 **파일명 처리 원칙:** - 제거/이스케이프 대상: `"`, `\r`, `\n`, `\0`, `/`, `\` - 최대 길이 제한 적용 - 비 ASCII 문자는 `filename*`에 RFC 5987 방식으로 인코딩 ```http Content-Disposition: attachment; filename="safe_name.ext"; filename*=UTF-8''original%20name.ext ``` **성능 / 운영 보호 정책:** - 사용자별 동시 다운로드 수 제한 - 공유 링크별 rate limit - 전체 서버 동시 스트림 수 제한 - 대용량 파일 전송 속도 제한 (필요 시) - `sendfile` / `X-Accel-Redirect` / `X-Sendfile` 활용 검토 ### 8단계: 로그 기록 / 카운트 증가 **카운트 증가 정책:** - **공유 링크 다운로드에만 적용** - `download_count`는 **세션 단위 전체 파일 다운로드 완료 시점**에만 증가 - 운영 분석을 위해 시도 수와 완료 수를 분리 집계 가능 | 카운트 항목 | 증가 시점 | 용도 | |------------|-----------|------| | `download_attempt_count` | 스트림 요청 시작 시점 | 시도 횟수 추적 | | `download_completed_count` | 전체 파일 전송 완료 시점 | 실제 완료 횟수 추적 | > 분리 집계를 통해 네트워크 중단, 사용자 취소, 실패율 등을 더 정확히 분석할 수 있다. --- ## 에러 응답 정리 | 상황 | HTTP 상태 코드 | |------|----------------| | 인증 실패 / 인증 토큰 만료 | `401 Unauthorized` | | 다운로드 세션 토큰 만료 / 위변조 | `401 Unauthorized` | | 파일 없음 또는 권한 없음 | `404 Not Found` | | Range 범위 초과 | `416 Range Not Satisfiable` | | 정상 전체 응답 | `200 OK` | | 정상 부분 응답 (Range) | `206 Partial Content` | --- ## 보안 체크리스트 | # | 항목 | 관련 단계 | |---|------|-----------| | 1 | JWT / share token 서명 검증 | 1단계 | | 2 | 파일 접근 권한 및 공유 상태 검증 | 2단계 | | 3 | 권한 없음과 파일 없음은 외부에 동일하게 `404` 처리 | 2단계 | | 4 | 파일 메타데이터 및 다운로드 가능 상태 사전 검증 | 3단계 | | 5 | 다운로드 세션 토큰 단명 발급 (권장 TTL: 약 5분) | 4단계 | | 6 | 세션 토큰은 파일 ID 및 요청자 식별자에 바인딩 | 4단계 | | 7 | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | 5단계 | | 8 | Path traversal 방어 (`realpath` + 루트 경로 검증) | 6단계 | | 9 | 실제 스트리밍 직전 파일 존재 및 open 가능 여부 재검증 | 6단계 | | 10 | `Content-Disposition: attachment` 강제 | 7단계 | | 11 | 파일명 sanitize 및 `filename*` 지원 | 7단계 | | 12 | MIME 타입은 서버 기준으로 안전하게 결정 | 7단계 | | 13 | `ETag` / `If-Range` 지원 | 7단계 | | 14 | 다운로드 로그 및 감사 로그 기록 | 8단계 | | 15 | 동시 다운로드 수 / rate limit / 대역폭 보호 정책 적용 | 7~8단계 | --- ## 설계 요약 이 설계는 다음 **6가지 목표**를 동시에 만족한다. | 목표 | 달성 방법 | |------|-----------| | **파일 존재 여부 노출 최소화** | 권한 없음 / 파일 없음 모두 `404` 통일 처리 | | **권한 검증 강화** | 인증 사용자·공유 링크 모두 다단계 검증 | | **불필요한 세션 발급 방지** | 세션 발급 전 다운로드 가능 상태 사전 검증 | | **브라우저 호환성 확보** | 세션 TTL 내 GET / Range / resume / 병렬 요청 허용 | | **재생 공격 위험 완화** | 단명 세션 토큰 + 파일·요청자 바인딩 | | **운영 환경 자원 보호** | 동시 다운로드 제한, rate limit, 대역폭 제어 |