diff --git a/packs-lab/app/routes.py b/packs-lab/app/routes.py index ae699ed..cd203ca 100644 --- a/packs-lab/app/routes.py +++ b/packs-lab/app/routes.py @@ -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: diff --git a/packs-lab/tests/test_routes.py b/packs-lab/tests/test_routes.py index 7985d59..3c13881 100644 --- a/packs-lab/tests/test_routes.py +++ b/packs-lab/tests/test_routes.py @@ -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",