fix(security): 프로젝트 경로가 붙는 API가 그 프로젝트의 회사를 대조하게 한다

B03~B07 라우터는 로그인·회사 소속만 확인하고 URL의 project_id가 누구 것인지는 보지
않았다. 프로젝트 id만 알면 남의 회사 자료를 읽고 쓸 수 있었고, B07 도각 저장(PUT)은
그 회사의 공용 양식을 덮어쓴다.

require_project_access를 protected_with_company에 함께 걸어 경로에 project_id가 있는
요청만 한 곳에서 검사한다. 다른 회사면 403, 없는 프로젝트면 404, 시스템 관리자는 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 16:11:24 +09:00
co-authored by Claude Opus 5
parent 5fc993e841
commit f82e60492e
2 changed files with 45 additions and 2 deletions
+33
View File
@@ -175,3 +175,36 @@ async def require_company(
if session["company_id"] is None and session["role"] != "SYSTEM_ADMIN":
raise HTTPException(status_code=403, detail="회사 연결이 필요합니다.")
return session
async def require_project_access(
request: Request, session: dict[str, Any] = Depends(verify_session)
) -> dict[str, Any]:
"""URL의 `project_id`가 **내 회사 것인지** 대조한다 (2026-09-01 신설).
지금까지 B03~B07 라우터는 로그인·회사 소속만 확인하고 URL의 프로젝트가 누구 것인지는
보지 않았다. 프로젝트 id만 알면 남의 회사 자료를 읽고 쓸 수 있었다 — 특히 B07 도각
저장은 그 회사의 공용 양식을 덮어쓴다.
`protected_with_company` 목록에 함께 걸어 두면 경로에 `project_id`가 있는 요청만
검사하고 나머지는 그대로 지나간다. 검사는 한 곳에서만 돈다.
"""
project_id = request.path_params.get("project_id")
if not project_id:
return session
if session["role"] == "SYSTEM_ADMIN":
return session
pool = get_db_pool()
async with pool.acquire() as connection, connection.cursor() as cursor:
await cursor.execute(
"SELECT company_id FROM projects WHERE id = %s AND deleted_at IS NULL",
(str(project_id),),
)
row = await cursor.fetchone()
if not row:
raise HTTPException(status_code=404, detail="프로젝트를 찾을 수 없습니다.")
if row[0] != session["company_id"]:
raise HTTPException(status_code=403, detail="다른 회사의 프로젝트입니다.")
return session
+12 -2
View File
@@ -49,7 +49,11 @@ from B06_Section.B06_Section_Router_Confirm import (
)
from B07_DesignDetail.B07_DesignDetail_Router import router as b07_design_router
from B08_Quantity.B08_Quantity_Router import router as b08_quantity_router
from common_util.common_util_auth import require_company, verify_session
from common_util.common_util_auth import (
require_company,
require_project_access,
verify_session,
)
from common_util.common_util_resource_monitor import sample_resources_loop
from common_util.common_util_temp_cleanup import cleanup_expired_temp_uploads_loop
from config.config_db import close_db_pool, get_db_pool, init_db_pool
@@ -366,7 +370,13 @@ app.include_router(a09_security_router)
app.include_router(b01_dashboard_router)
app.include_router(b02_proj_register_router)
protected = [Depends(verify_session)]
protected_with_company = [Depends(verify_session), Depends(require_company)]
# 프로젝트 경로가 붙는 라우터는 그 프로젝트가 내 회사 것인지도 함께 본다
# (2026-09-01 — 지금까지 id만 알면 남의 회사 자료에 닿았다).
protected_with_company = [
Depends(verify_session),
Depends(require_company),
Depends(require_project_access),
]
app.include_router(b03_file_input_router, dependencies=protected_with_company)
app.include_router(b03_temp_upload_router, dependencies=protected)
app.include_router(b03_temp_attach_router, dependencies=protected_with_company)