fix(packs-lab): sign-link path traversal — startswith → relative_to (CODE_REVIEW F1)

str(abs_path).startswith(str(PACK_HOST_DIR))는 trailing slash가 없어
sibling 경로(/foo/packs ↔ /foo/packs_evil)를 통과시켜 DSM API에 잘못된
호스트 경로를 전달할 수 있었음. Path.relative_to 기반으로 컴포넌트 단위
엄격 검증으로 교체. test_sign_link_rejects_sibling_path 회귀 테스트
추가 (RED → GREEN 검증).
This commit is contained in:
2026-05-17 13:50:22 +09:00
parent 64fbbb7958
commit 1e5e1bcdff
2 changed files with 28 additions and 1 deletions

View File

@@ -133,8 +133,12 @@ async def sign_link(
# 경로 안전: PACK_HOST_DIR(NAS 호스트 절대경로) 하위인지 확인.
# file_path는 upload 라우트가 Supabase에 저장한 호스트경로 그대로 전달되어 DSM API에 사용됨.
# str.startswith는 '/foo/packs' 와 '/foo/packs_evil' 같은 sibling 경로를 통과시키므로
# Path.relative_to로 엄격하게 컴포넌트 단위 검증한다 (CODE_REVIEW F1).
abs_path = Path(payload.file_path).resolve()
if not str(abs_path).startswith(str(PACK_HOST_DIR)):
try:
abs_path.relative_to(PACK_HOST_DIR.resolve())
except ValueError:
raise HTTPException(status_code=400, detail="허용된 경로 외부")
try:

View File

@@ -60,6 +60,29 @@ def test_sign_link_path_outside_base():
assert r.status_code == 400
def test_sign_link_rejects_sibling_path():
"""PACK_HOST_DIR='/foo/packs' 일 때 '/foo/packs_evil/x.mp4' 같이 prefix만
통과하는 sibling 경로는 거부해야 한다 (CODE_REVIEW F1, path traversal 변형).
기존 str.startswith 방식은 trailing slash가 없어 sibling 경로를 통과시킴.
relative_to 기반 검증으로 교체되어야 통과한다.
"""
import json as _json
from pathlib import Path
base_resolved = Path("/foo/packs").resolve()
# base의 자식이 아닌 sibling 경로 (예: /foo/packs_evil/...)
sibling_posix = (base_resolved.parent / f"{base_resolved.name}_evil" / "x.mp4").as_posix()
with patch("app.routes.PACK_HOST_DIR", base_resolved):
body = _json.dumps(
{"file_path": sibling_posix, "expires_in_seconds": 14400}
).encode()
r = client.post("/api/packs/sign-link", content=body, headers=_signed(body))
assert r.status_code == 400, (
f"sibling 경로 '{sibling_posix}'가 허용됨 (status={r.status_code}) "
f"— path traversal 가능성"
)
def test_upload_invalid_token():
r = client.post(
"/api/packs/upload",