# task-2829 보고서 — PR-D0 CORS env 값 검증 (fail-closed 강화)

- **팀**: dev1-team (헤르메스 팀장, 불칸 백엔드, 아르고스 테스터)
- **작업 레벨**: Lv.2 (micro / merge-forbidden)
- **대상 리포/브랜치**: `/home/jay/projects/InsuRo`, `task/task-2828-dev1` (PR #117 이어서 커밋, 새 PR/브랜치 없음)
- **일자**: 2026-07-22

---

## Situation (상황)
PR #117(PR-D0, V 레코드 GET 조회 + 확장 CORS)에서 `server/main.py` CORS 설정이
`INSURO_EXTENSION_ORIGIN` env 값을 **형식 검증 없이** `_cors_allow_origins` 에 추가하고
`allow_credentials=True` 로 CORS 를 연다. 미설정 fail-closed 는 있으나 **잘못 설정 fail-closed 가 없다.**

## Complication (문제 — Codex HOLD)
env 가 오타·과도한 값(`*`, 비-extension origin, `chrome-extension://` 뒤 잘못된 ID, 다중값 등)으로
설정되면 **자격증명 허용(credentialed) CORS 가 그 origin 에 그대로 열린다.** 자격증명 CORS + 와일드카드/임의 origin 은
민감정보 유출 위험. "미설정"만 fail-closed 이고 "**잘못 설정**"은 fail-open 인 상태.

## Question (해결 과제)
`INSURO_EXTENSION_ORIGIN` 을 `allow_origins` 에 추가하기 전에 **형식 검증**을 넣어,
표준 Chrome 확장 origin 형식만 허용하고 그 외 모든 값은 **거부(추가 안 함 = fail-closed)** 한다.
기존 웹앱 origin 목록·`allow_credentials` 동작 회귀 0.

## Answer (해결)
`server/main.py` CORS 블록(311~324줄)에 모듈 레벨 정규식 상수 + `fullmatch` 검증 추가:

```python
# 표준 Chrome 확장 origin 형식(chrome-extension:// + 32자 소문자 a~p ID)만 허용한다.
# 형식 위반(*, 비-extension, 잘못된 ID, 다중값 등)이면 추가하지 않는다(= fail-closed).
_CHROME_EXTENSION_ORIGIN_RE = re.compile(r"chrome-extension://[a-p]{32}")
_insuro_ext_origin = os.getenv("INSURO_EXTENSION_ORIGIN", "").strip()
if _insuro_ext_origin:
    if _CHROME_EXTENSION_ORIGIN_RE.fullmatch(_insuro_ext_origin):
        _cors_allow_origins.append(_insuro_ext_origin)
    else:
        logger.warning(
            "INSURO_EXTENSION_ORIGIN 형식위반이라 CORS 미허용(fail-closed): %r",
            _insuro_ext_origin,
        )
```

- 표준 Chrome 확장 ID 형식(`chrome-extension://` + 정확히 32자, 각 글자 소문자 a~p) `fullmatch` 만 허용.
- `*`·와일드카드·비-chrome-extension·잘못된 ID(길이/대문자/숫자/범위밖)·다중값(콤마/공백) → **미추가 + 경고 로그**.
- 미설정/빈값 → 기존처럼 웹앱 origin 만 허용.
- 기존 웹앱 origin 목록·`allow_credentials=True` 무변경(회귀 0).

## 수정/생성 파일 목록 (task-2829 커밋 2개 범위)
- `server/main.py` (+10/-1) — 커밋 `63bdc54` (불칸)
- `server/tests/test_consultation_history_get.py` (+29) — 커밋 `641de4a` (아르고스)
- **`server/main.py` + `tests/**` 외 변경 0** (`git diff --stat` 로 확인). forbidden_paths(extension/src/.github/routes/schemas/migrations) 무변경.

## 테스트 결과
### 단위/회귀 (pytest)
- 신규 `test_cors_fail_closed_on_malformed_ext_origin` 파라미터라이즈 6 케이스(`*`, `http://evil.com`, 짧은 ID, 너무 긴 ID+대문자, `q`(a-p 범위밖), 다중값) 전부 PASS.
- 기존 CORS 3 테스트(미설정 fail-closed / 유효값 포함 / 웹앱 preflight 회귀0) 전부 PASS.
- `pytest tests/test_consultation_history_get.py -q` → **21 passed, 4 warnings** (warning 은 기존 `on_event` DeprecationWarning, 이번 변경 무관). **회귀 감소 0.**

### L1 스모크테스트 (실서버 기동 + curl preflight — 필수)
- **서버 재시작: 성공** (uvicorn `main:app`, 127.0.0.1, 실제 부팅 "Application startup complete" 확인)
- **API 응답 확인 (curl preflight):**
  - 유효 확장 origin(`chrome-extension://abcdefghijklmnopabcdefghijklmnop`) 설정 시:
    - 유효 확장 origin → `access-control-allow-origin` **부여됨(허용)** ✓
    - `http://evil.com` → ACAO **없음(거부)** ✓
    - `https://insuro.biz`(웹앱) → ACAO **부여됨(회귀0)** ✓
  - **잘못 설정(`INSURO_EXTENSION_ORIGIN='*'`) 시 (이번 fix 핵심):**
    - 서버 로그: `INSURO_EXTENSION_ORIGIN 형식위반이라 CORS 미허용(fail-closed): '*'` ✓
    - `http://evil.com` → ACAO 없음(거부) ✓
    - 임의 chrome-extension origin → ACAO 없음(잘못설정이라 확장 미허용) ✓
    - `https://insuro.biz`(웹앱) → ACAO 부여됨(회귀0) ✓
- **스크린샷: 해당없음** (백엔드 API 작업, curl 결과로 대체)
- **판정: L1 통과** — 잘못 설정 env 도 fail-closed 됨을 실서버로 입증.

### 실동작 판정 루프(격리 서브프로세스, import 시점 검증)
6개 입력 중 유효 32자 a-p 만 `_cors_allow_origins` 에 확장 origin 추가(True), 나머지 5건 전부 미추가(False).

## 버그 유무
- 신규 버그 없음. 발견 이슈 없음.
- pyright `Import "main"/"sb_helpers" 등 could not be resolved` 경고는 이 repo 의 기존 런타임 `sys.path` 패턴(테스트가 서버 디렉토리 cwd 에서 `import main`)으로, 이번 변경과 무관한 사전 존재 진단.

## 게이트
- **G1(설계)**: affected_files = `server/main.py`, `server/tests/**` — allowed_resources 내, 다른 팀 겹침 없음.
- **G2(구현)**: 팀 QC(불칸 구현 셀프검증 + 아르고스 테스트 21 passed) + 팀장 L1 실서버 스모크 통과.
- **G3(머지)**: PR #117 에 push 완료(MERGEABLE). **Gemini Code Assist sunset(2026-07-22)** 으로 자동 리뷰 산출 불가 → 머지판정은 ANU 독립검증으로 대체(L1 실서버 + 회귀0 이미 통과). **본 task 는 머지 금지** — PR open 유지, 머지는 아누/회장 재정합(Codex) 후 결정.

## 머지 판단
- **머지 필요**: No (task 명시 머지 금지 — PR #117 open 유지)
- **브랜치**: `task/task-2828-dev1`
- **워크트리 경로**: `/home/jay/projects/InsuRo/.worktrees/task-2828-dev1`
- **머지 의견**: 코드 범위 최소(+10/-1 + 테스트 +29), 회귀 0, L1 실서버로 잘못설정 fail-closed 입증. PR #117 본체(GET)와 함께 Codex 재정합 시 안전하게 머지 가능. 단독 머지 결정은 아누/회장 소관.

## 모델 사용 기록
- 불칸(백엔드) 구현: sonnet (로직 구현)
- 아르고스(테스터) 테스트: sonnet (테스트 설계/작성)
- haiku 미사용. 팀장(Opus)은 설계/검토/통합/L1 검증만 수행, 직접 코딩 없음.

## 세션 통계
- 총 도구 호출: 0회

