10 KiB
개요
이 문서는 인증 사용자와 공유 링크 사용자 모두를 지원하는 파일 다운로드 시스템의 전체 흐름을 정의한다. 핵심 구조는 다음과 같다:
권한 검증 → 다운로드 가능 상태 사전 검증 → 다운로드 세션 발급 → 최종 경로 검증 및 스트리밍
전체 흐름 (8단계)
!
단계별 상세
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 요청
응답 예시:
{
"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단계: 세션 토큰으로 파일 스트림 요청
클라이언트 요청 예시:
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 및 최종 검증
목적: 스트림을 열기 직전에 실제 파일 경로를 확정하고 최종 검증을 수행한다.
처리 절차:
- 키-경로 맵(DB 또는 설정) 조회 → 절대 경로 획득
realpath()등으로 경로 정규화- 정규화된 경로가 허용된 루트 디렉터리 내부인지 검증 (예:
/data/uploads/) - 실제 파일 존재 여부 확인
- 파일 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 타입 처리 원칙:
- 서버 측 MIME 판별 사용 (클라이언트 업로드 값 불신)
- 저장된 메타데이터와 확장자는 보조적으로 사용
- 판별 불확실 시
application/octet-stream - XSS 가능 타입이라도
Content-Disposition: attachment로 강제
파일명 처리 원칙:
- 제거/이스케이프 대상:
",\r,\n,\0,/,\ - 최대 길이 제한 적용
- 비 ASCII 문자는
filename*에 RFC 5987 방식으로 인코딩
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, 대역폭 제어 |