From 75434469010e91a71653366ea9e971a2f0ca1cf8 Mon Sep 17 00:00:00 2001 From: son Date: Mon, 16 Mar 2026 17:57:51 +0900 Subject: [PATCH] add flows --- 기획/기획서.md | 5 +- 설계/flows/다운로드 흐름.md | 136 ++++++++++++++++++++++++++++++++ 설계/img/file_download_flow.svg | 121 ++++++++++++++++++++++++++++ 설계/무제.md | 0 4 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 설계/flows/다운로드 흐름.md create mode 100644 설계/img/file_download_flow.svg delete mode 100644 설계/무제.md diff --git a/기획/기획서.md b/기획/기획서.md index 1f326a6..d2dddfb 100644 --- a/기획/기획서.md +++ b/기획/기획서.md @@ -281,7 +281,7 @@ Nextcloud의 핵심 기능(파일 관리/공유/미리보기)을 제공하되, * --- -## 6) 핵심 설계 원칙 (중요) +## 6) 핵심 설계 원칙 ### 원칙 1. “사용자 폴더 구조”와 “실제 디스크 경로”를 분리한다 @@ -298,7 +298,7 @@ MVP가 로컬FS라도, 실제 디스크에 사용자 폴더 구조를 그대로 - 충돌/인코딩/특수문자 이슈 감소 -#### 권장 방식 +#### 방식 - **DB에는 논리 폴더 구조 저장** @@ -694,7 +694,6 @@ tus 업로드는 중간 상태가 있으므로 업로드 세션과 최종 파일 4. 파일 스트림 응답 (Range 지원 권장) 5. 로그 기록 / 카운트 증가(공유 링크일 경우) - --- diff --git a/설계/flows/다운로드 흐름.md b/설계/flows/다운로드 흐름.md new file mode 100644 index 0000000..311e7a5 --- /dev/null +++ b/설계/flows/다운로드 흐름.md @@ -0,0 +1,136 @@ + +![[file_download_flow.svg]] +## 단계별 상세 + +### 1. 사용자 인증 / 공유 링크 접근 + +요청의 진입점으로, 두 가지 접근 경로를 처리한다. + +- **인증 사용자**: `Authorization` 헤더의 JWT Bearer 토큰을 파싱 및 검증 +- **공유 링크**: URL 쿼리 파라미터의 `share_token`을 파싱 + +두 경로 모두 이후 단계에서 동일한 인터페이스로 처리될 수 있도록 내부적으로 정규화한다. + +--- + +### 2. 파일 권한 / 공유 상태 검증 + +DB를 조회하여 요청자가 해당 파일에 접근할 수 있는지 확인한다. + +- 인증 사용자: 파일의 소유자 또는 공유 대상 여부 확인 +- 공유 링크: 링크의 활성화 여부, 만료 여부, 비밀번호 보호 여부 확인 +- 권한이 없으면 즉시 **`403 Forbidden`** 반환 (이후 단계로 진행하지 않음) + +##### Risk +권한이 없는 경우 `403`, 파일이 없는 경우 `404`를 분리 반환한다. 이 차이를 이용하면 공격자가 파일 ID를 열거하여 존재하는 파일 목록을 추론할 수 있다. + +--- + +### 3. 다운로드 토큰 발급 + +실제 스트리밍 요청에 사용할 단명(short-lived) 토큰을 생성한다. + +- TTL은 **약 60초** 권장 (재사용·탈취 위험 최소화) +- 토큰에는 파일 ID, 요청자 식별자, 만료 시각을 포함 +- HMAC 서명 또는 JWT로 위변조 방지 + +``` +# Redis 기반 원자적 무효화 +result = REDIS.SET(f"dl_token:{token}", "used", NX=True, EX=60) +# NX: key가 없을 때만 SET → 원자적 보장 +# result가 None이면 이미 사용된 토큰 → 401 + +``` + +> API 서버와 스트리밍 서버를 분리 운영할 때 특히 유용하다. +> 클라이언트는 이 토큰을 받아 스트리밍 엔드포인트에 직접 요청한다. + +--- + +### 4. 다운로드 토큰으로 파일 스트림 요청 + +클라이언트가 발급받은 토큰으로 스트리밍 엔드포인트에 요청한다. + +``` +GET /files/stream?token={download_token} +Range: bytes=0-1048575 (선택, Range 요청 시) +``` + +- 토큰이 만료되었거나 유효하지 않으면 **`401 Unauthorized`** 반환 +- 토큰은 1회 사용 후 무효화하는 것을 권장 (재생 공격 방어) + +#### Risk +- "토큰은 1회 사용 후 무효화하는 것을 권장"이라고만 기술되어 있으나, 동시에 같은 토큰으로 2개 이상의 요청이 들어오는 경우의 원자적 무효화 전략이 없다. +-  토큰을 1회 사용 후 무효화하면, 대용량 파일의 Range 기반 이어받기(resume)가 불가능해진다. 네트워크 끊김 후 클라이언트가 `Range: bytes=10485760-`으로 재요청하면, 이미 무효화된 토큰이므로 `401`이 반환된다. +- +--- + +### 5. storage_key → 로컬 FS 경로 resolve + +`storage_key`를 실제 파일 시스템 경로로 변환한다. + +- 키-경로 맵(DB 또는 설정 파일)을 조회하여 절대 경로 획득 +- **Path traversal 방어 필수**: `realpath()` 등으로 경로를 정규화하고, 허용된 루트 디렉터리(`/data/uploads/` 등) 내에 있는지 반드시 검증 +- 파일이 존재하지 않으면 **`404 Not Found`** 반환 + +--- + +### 6. 파일 스트림 응답 (Range 지원 권장) + +파일을 청크 단위로 스트리밍하여 응답한다. + +**응답 헤더:** + +|헤더|값| +|---|---| +|`Content-Type`|파일 MIME 타입| +|`Content-Disposition`|`attachment; filename="파일명"`| +|`Accept-Ranges`|`bytes`| +|`Content-Length`|파일 크기 (전체 또는 청크)| + +**Range 지원 시 동작:** + +- `Range` 헤더가 있으면 **`206 Partial Content`** 로 응답 +- `Range` 헤더가 없으면 **`200 OK`** 로 전체 파일 응답 +- Range를 지원하면 대용량 파일 resume, 미디어 스트리밍, 멀티파트 다운로드 모두 대응 가능 + + +#### Risk +- 응답 헤더에 `Content-Type: 파일 MIME 타입`이라고만 되어 있고, 이 값을 **어디서 가져오는지** 명시되어 있지 않다. 업로드 시 클라이언트가 보낸 값을 DB에 저장하고 그대로 사용하는 경우, 공격자가 `text/html`이나 `image/svg+xml`로 위장한 악성 파일을 업로드할 수 있다. +-  로컬 FS에서 직접 스트리밍하므로 API/스트리밍 서버의 egress 대역폭이 병목이 된다. 동시 다운로드 수 제한, 사용자별 대역폭 제한, 전체 서버 대역폭 보호 정책이 없다. + + +--- + +### 7. 로그 기록 / 카운트 증가 + +다운로드 완료 후 감사(Audit) 로그를 기록하고, 공유 링크의 경우 카운트를 증가시킨다. + +- **로그 항목**: 파일 ID, 요청자, IP, User-Agent, 다운로드 시각, 파일 크기 +- **카운트 증가**: 공유 링크에만 적용 (`download_count++`) +- **타이밍 주의**: 스트림이 **완전히 전송된 이후**에 카운트를 올린다. 요청 시점에 올리면 중단된 다운로드도 집계된다. + +--- + +## 에러 응답 정리 + +|상황|HTTP 상태 코드| +|---|---| +|인증 실패 / 토큰 만료|`401 Unauthorized`| +|권한 없음|`403 Forbidden`| +|파일 없음|`404 Not Found`| +|Range 범위 초과|`416 Range Not Satisfiable`| +|정상 전체 응답|`200 OK`| +|정상 부분 응답 (Range)|`206 Partial Content`| + +--- + +## 보안 체크리스트 + +- [ ] JWT / share_token 서명 검증 +- [ ] 파일 접근 권한 DB 조회 (캐시 사용 시 TTL 주의) +- [ ] 다운로드 토큰 단명 발급 (TTL ≤ 60s) +- [ ] 토큰 1회 사용 후 무효화 +- [ ] Path traversal 방어 (`realpath` + 루트 경로 검증) +- [ ] `Content-Disposition: attachment` 헤더 명시 +- [ ] 다운로드 완료 후 감사 로그 기록 \ No newline at end of file diff --git a/설계/img/file_download_flow.svg b/설계/img/file_download_flow.svg new file mode 100644 index 0000000..a564223 --- /dev/null +++ b/설계/img/file_download_flow.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + 1. 사용자 인증 / 공유 링크 접근 + JWT 토큰 또는 share_token 파싱 + + + + + + + + 2. 파일 권한 / 공유 상태 검증 + DB 조회 → 접근 허용 여부 확인 + + + + + + + 403 Forbidden + + + + + + + + 3. 다운로드 토큰 발급 + 단기 서명 토큰 생성 (TTL ~60s) + + + + + + + + 4. 토큰으로 파일 스트림 요청 + GET /stream?token=… 호출 + + + + + + + 401 Expired + + + + + + + + 5. storage_key → 로컬 FS 경로 resolve + 키-경로 맵 조회 / path traversal 방어 + + + + + + + 404 Not Found + + + + + + + + 6. 파일 스트림 응답 + 206 Partial Content (Range 지원) + + + + + + + + 7. 로그 기록 / 카운트 증가 + 공유 링크인 경우 download_count++ + + + + + + 클라이언트 + + + + Auth / ACL + + + + Token Service + + + + Storage Layer + + + + Stream Handler + + + + Audit Logger + + + + +정상 흐름 → + +에러 분기 → + \ No newline at end of file diff --git a/설계/무제.md b/설계/무제.md deleted file mode 100644 index e69de29..0000000