From 1e5e1bcdff634be75ee59df5c1f7321e20b9ba13 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 17 May 2026 13:50:22 +0900 Subject: [PATCH 1/3] =?UTF-8?q?fix(packs-lab):=20sign-link=20path=20traver?= =?UTF-8?q?sal=20=E2=80=94=20startswith=20=E2=86=92=20relative=5Fto=20(COD?= =?UTF-8?q?E=5FREVIEW=20F1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 검증). --- packs-lab/app/routes.py | 6 +++++- packs-lab/tests/test_routes.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) 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", From 6053e69afcef7ed2f7e9fdd2f9ffd4209ec930a5 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 17 May 2026 13:53:50 +0900 Subject: [PATCH 2/3] =?UTF-8?q?fix(stock):=20admin=20API=20auth=20hardenin?= =?UTF-8?q?g=20=E2=80=94=20ADMIN=5FAPI=5FKEY=20=EB=B9=88=20=EA=B0=92=20?= =?UTF-8?q?=EC=8B=9C=20503=20=EA=B1=B0=EB=B6=80=20(CODE=5FREVIEW=20F2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 운영 .env에 ADMIN_API_KEY가 누락되면 verify_admin이 무조건 통과해서 /api/trade/balance, /api/trade/order 인증이 무력화되던 문제 차단. - ADMIN_API_KEY 설정 + 올바른 키 → 통과 (기존 동작) - ADMIN_API_KEY 설정 + 잘못된 키 → 401 (기존 동작) - ADMIN_API_KEY 미설정 + ALLOW_UNAUTHENTICATED_ADMIN=true → 통과 (dev mode) - ADMIN_API_KEY 미설정 + dev flag 없음 → 503 (신규, 운영 보호) .env.example에 신규 ALLOW_UNAUTHENTICATED_ADMIN=false 안내 추가. stock/pytest.ini 신규 (pythonpath=. 설정으로 tests 모듈 import 가능). test_admin_auth.py 4 케이스 (RED → GREEN 검증, regression 포함). --- stock/app/main.py | 23 +++++++++++++++--- stock/pytest.ini | 3 +++ stock/tests/test_admin_auth.py | 43 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 stock/pytest.ini create mode 100644 stock/tests/test_admin_auth.py diff --git a/stock/app/main.py b/stock/app/main.py index 94b6968..4ae084a 100644 --- a/stock/app/main.py +++ b/stock/app/main.py @@ -47,13 +47,30 @@ scheduler = BackgroundScheduler(timezone=os.getenv("TZ", "Asia/Seoul")) # Windows AI Server URL (NAS .env에서 설정) WINDOWS_AI_SERVER_URL = os.getenv("WINDOWS_AI_SERVER_URL", "http://192.168.0.5:8000") -# Admin API Key 인증 +# Admin API Key 인증 — /api/trade/* 보호 (CODE_REVIEW F2) +# 빈 키 + 명시적 dev flag 없으면 503으로 거부. 운영 .env에 ADMIN_API_KEY 누락 시 +# 무인증 통과되던 버그 차단. ADMIN_API_KEY = os.getenv("ADMIN_API_KEY", "") def verify_admin(x_admin_key: str = Header(None)): - """admin/trade 엔드포인트 보호용 API 키 검증""" + """admin/trade 엔드포인트 보호용 API 키 검증. + + - ADMIN_API_KEY 설정됨 + 키 일치 → 통과 + - ADMIN_API_KEY 설정됨 + 키 불일치 → 401 Unauthorized + - ADMIN_API_KEY 미설정 + ALLOW_UNAUTHENTICATED_ADMIN=true → 통과 (개발 모드) + - ADMIN_API_KEY 미설정 + dev flag 없음 → 503 (보호 강화, 운영 .env 누락 차단) + """ if not ADMIN_API_KEY: - return # 키 미설정 시 인증 비활성화 (개발 환경) + if os.getenv("ALLOW_UNAUTHENTICATED_ADMIN", "false").lower() == "true": + return # 개발 환경 명시적 허용 + raise HTTPException( + status_code=503, + detail=( + "admin endpoint protected — ADMIN_API_KEY not configured. " + "Set ADMIN_API_KEY in .env, or set ALLOW_UNAUTHENTICATED_ADMIN=true " + "for development only." + ), + ) if x_admin_key != ADMIN_API_KEY: raise HTTPException(status_code=401, detail="Unauthorized") diff --git a/stock/pytest.ini b/stock/pytest.ini new file mode 100644 index 0000000..bdfc0eb --- /dev/null +++ b/stock/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = . +asyncio_mode = auto diff --git a/stock/tests/test_admin_auth.py b/stock/tests/test_admin_auth.py new file mode 100644 index 0000000..b612518 --- /dev/null +++ b/stock/tests/test_admin_auth.py @@ -0,0 +1,43 @@ +"""verify_admin 보안 강화 회귀 테스트 (CODE_REVIEW F2). + +운영 .env에서 ADMIN_API_KEY가 누락되면 /api/trade/balance, /api/trade/order +인증이 무력화되는 버그를 막기 위한 가드. +""" +import os +from unittest.mock import patch + +import pytest +from fastapi import HTTPException + +from app import main as stock_main + + +def test_verify_admin_rejects_when_key_missing_and_no_dev_flag(monkeypatch): + """ADMIN_API_KEY 미설정 + ALLOW_UNAUTHENTICATED_ADMIN 미설정 → 503.""" + monkeypatch.setattr(stock_main, "ADMIN_API_KEY", "") + monkeypatch.delenv("ALLOW_UNAUTHENTICATED_ADMIN", raising=False) + with pytest.raises(HTTPException) as exc_info: + stock_main.verify_admin(x_admin_key=None) + assert exc_info.value.status_code == 503 + assert "ADMIN_API_KEY" in exc_info.value.detail + + +def test_verify_admin_allows_when_key_missing_with_dev_flag(monkeypatch): + """ADMIN_API_KEY 미설정 + ALLOW_UNAUTHENTICATED_ADMIN=true → 통과 (개발 모드).""" + monkeypatch.setattr(stock_main, "ADMIN_API_KEY", "") + monkeypatch.setenv("ALLOW_UNAUTHENTICATED_ADMIN", "true") + stock_main.verify_admin(x_admin_key=None) # 예외 없으면 통과 + + +def test_verify_admin_rejects_wrong_key(monkeypatch): + """ADMIN_API_KEY 설정 + 잘못된 키 → 401 (regression).""" + monkeypatch.setattr(stock_main, "ADMIN_API_KEY", "secret123") + with pytest.raises(HTTPException) as exc_info: + stock_main.verify_admin(x_admin_key="wrong") + assert exc_info.value.status_code == 401 + + +def test_verify_admin_allows_correct_key(monkeypatch): + """ADMIN_API_KEY 설정 + 올바른 키 → 통과 (regression).""" + monkeypatch.setattr(stock_main, "ADMIN_API_KEY", "secret123") + stock_main.verify_admin(x_admin_key="secret123") # 예외 없으면 통과 From 49c5c57be594ee9e0d3ffe9403ce0912a3c7c5c0 Mon Sep 17 00:00:00 2001 From: gahusb Date: Sun, 17 May 2026 13:58:24 +0900 Subject: [PATCH 3/3] docs(env): add ALLOW_UNAUTHENTICATED_ADMIN guidance for F2 --- .env.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 78a4efa..33093f0 100644 --- a/.env.example +++ b/.env.example @@ -51,9 +51,14 @@ PGID=1000 # Windows AI Server (NAS 입장에서 바라본 Windows PC IP) WINDOWS_AI_SERVER_URL=http://192.168.45.59:8000 -# Admin API Key (trade/order 등 민감 엔드포인트 보호, 미설정 시 인증 비활성화) +# Admin API Key — /api/trade/* 등 민감 엔드포인트 보호. +# 운영 .env에는 반드시 값을 채워야 함. 빈 값이면 503 응답으로 거부됨 (CODE_REVIEW F2). ADMIN_API_KEY= +# 개발 모드: 위 ADMIN_API_KEY 비워둔 채로 trade/admin 엔드포인트 호출 허용. +# 운영 환경에서는 절대 true로 두지 말 것. 기본 false (보호 활성). +ALLOW_UNAUTHENTICATED_ADMIN=false + # Anthropic API Key (AI Coach 프록시 + 뉴스 요약 Claude provider) ANTHROPIC_API_KEY= ANTHROPIC_MODEL=claude-haiku-4-5-20251001