# task-2788+1 — A-0 라이브 실행 전 보안 preflight micro-fix (BLOCKER)

## 레벨
Lv.2 (보안 차단 수정). **same-branch `task/task-2788-dev5` 계속 커밋. PR·merge·배포 금지.**

## 단일 소스
`memory/specs/ohmy_product_premium_data_contract_v1_260718.md` + `memory/tasks/task-2788.md`

## 배경 (GPT 지적 → ANU 코드확인 → Codex 라인확인, 3자 합의)
A-0 스파이크는 성공적(`url_construction: SOLVED`). 그러나 **실제 JWT를 붙여 라이브 실행하기 전 반드시 막아야 할 결함**이 있다:
- `background.js` `DIRECT_FETCH_SPIKE_RUN` 핸들러(191-200)가 **`msg.url` 을 무검증**으로 `runDirectFetchSpike()` 에 전달 → `fetch(url, {headers:{Authorization:"Bearer "+token}})`(315-342) 실행.
- `(msg, _sender, sendResponse)` — **sender 미검증**.
→ 확장에 메시지를 주입할 수 있는 경로가 있으면 **임의 URL + 실 JWT** 조합으로 **토큰이 외부로 유출**될 수 있다. **라이브 실행 전 hard blocker.**

## ★ 불변식 (유지)
서버 전송 0 · 토큰 저장/로깅 0 · 클릭 전 무동작 · MAIN world 인터셉터 추가 금지 · inject.js 미수정 · PR/merge/배포 금지.

## allowed_resources
```yaml
allowed_resources:
  paths:
    - "extension/background.js"
    - "extension/__tests__/direct_fetch_spike.test.ts"
    - "memory/reports/task-2788+1.md"
  forbidden_paths: ["src/**","server/**","supabase/**","**/migrations/**",".github/**",".env*","extension/inject.js","extension/manifest.json","extension/content.js"]
  new_file_limit: 0
  commands: ["git","npm","node","npx"]
  merge_policy: "none"
  ttl_hours: 5
```

## 수정 요구

### S1. URL allowlist (fetch 전 강제)
`runDirectFetchSpike` **호출 전에** `msg.url` 을 `new URL()` 로 파싱해 아래 **전부** 만족할 때만 진행:
- `protocol === "https:"`
- `hostname === "mmlfcp.ohmymanager.com"` (**정확 일치**. 와일드카드·endsWith 금지 — `evil-mmlfcp.ohmymanager.com.attacker.com` 우회 차단)
- `pathname === "/api/ProductPremiums"` (**정확 일치**, 대소문자 정규화 후 비교 가능하나 경로 조작 금지)
하나라도 불일치 → **`runDirectFetchSpike()` 자체를 호출하지 않고** `{rejected:"URL_NOT_ALLOWED"}` 반환. **토큰 미사용.**

### S2. sender 검증 (Codex 지적 — `sender.tab?.url` 만으로 불충분)
`_sender` → `sender` 로 실제 사용. 아래 **전부** 만족할 때만 진행:
- `sender.id === chrome.runtime.id` (보조 체크)
- **`sender.url` 또는 `sender.origin` 의 origin 이 `https://mmlfcp.ohmymanager.com`** (주 체크 — 실제 발신 문서 기준)
불일치 → **호출 안 함** + `{rejected:"SENDER_NOT_ALLOWED"}`. **토큰 미사용.**

### S3. token 최소 검증
`msg.token` 이 **존재하고 문자열이며 JWT 형태(점 2개로 3파트)** 인지만 확인. 아니면 `{rejected:"TOKEN_MALFORMED"}` 반환 후 중단. **원문 로깅 절대 금지**(길이·파트수만).

### S4. 실패 시 토큰 잔존 금지
거부·실패 경로에서 토큰을 **에러 객체·재시도 큐·verdict·로그 어디에도 남기지 않음**.

### S5. `cors_error` 명칭 정정 (GPT 지적)
`buildSpikeVerdict`(250-255)가 `TypeError` 를 전부 `cors_error` 로 묶어 **DNS·TLS·연결실패와 CORS 를 구분 못 함**.
→ 필드명을 **`transport_error`** 로 변경하고, 원인 분류를 **별도 필드**로 분리(`transport_error_kind`: `"CORS_SUSPECTED"|"DNS_OR_CONNECT"|"TLS"|"UNKNOWN"` — 단정 불가하면 `UNKNOWN`, **추측 금지**).

## 검증 (Codex 지정 — 라이브 실행 전 필수)
`direct_fetch_spike.test.ts` 에 아래 분기 테스트 **전부** 추가:
- allowlist reject: 다른 host / 다른 path / http / 유사도메인(`...ohmymanager.com.evil.com`) → 각각 거부 + **fetch 미호출** + 토큰 미사용
- valid sender 통과 / invalid sender(다른 origin, sender.url 부재) 거부
- token malformed 거부
- `transport_error` + `transport_error_kind` 분기
- 거부 응답·verdict·로그에 **토큰 문자열 미포함** assert
기존 테스트 전부 유지. **ANU 가 `npx vitest run` 전체 독립 재실행.**

## goal_assertions
- `npx vitest run`

## STOP_REPORT 조건
sender.url/origin 이 구조적으로 확보 불가 · allowlist 적용 시 정상 경로가 막힘 · content.js 수정이 필요(→ 별도 승인)

## 금지
서버 전송 · 토큰 저장/로깅 · 자동 실행 · 인터셉터 추가 · inject.js/manifest/content.js 수정 · PR·merge·배포 · 671줄 스파이크의 production 이식(별도 task).

## 산출물 & 보고
1. same-branch 커밋. PR 금지.
2. 보고: diff / S1~S5 각 근거 / 거부 분기 테스트 결과 / vitest 카운트 / MATCH·GAP.
3. 봇 종료 전 ANU callback(envelope만·UTF-8 ≤3900B, collector=ANU key c119085addb0f8b7).