# task-2991 — InsuRo 상담 RLS caller-binding (익명 무제한 접근 차단)

**레벨**: Lv.3 (프로덕션 DDL · 보안) · **팀**: dev3-team (다그다) · **level=security** · **회장 승인 완료** (2026-08-20)

## 이 명세의 근거
아래 모든 수치는 **ANU 가 프로덕션 DB 에 직접 접속해 실측**한 값이다(psycopg2 + `aws-1-ap-northeast-2.pooler.supabase.com:5432` session mode).
착수 전에 **각 항목을 직접 재확인**하라. 불일치하면 구현하지 말고 정정 보고하라.

## 배경 — 익명 사용자가 남의 상담을 읽고 쓸 수 있다
ANU 가 anon 키로 **타인 상담 메시지 본문 3건 직독을 재현**했다. 추정이 아니라 실증이다.

### 실측된 결함 6건
```
① customer_chat_tokens   SELECT {anon}  USING(true)      ← 아무 토큰이나 조회
② conversations          SELECT {anon}  USING(true)      ← 아무 대화나 조회
③ conversations          UPDATE {anon}  USING(true)      ← 아무 대화나 수정
④ conversation_messages  SELECT {anon}  USING(true)      ← 아무 본문이나 조회
⑤ conversation_messages  INSERT {anon}  WITH CHECK(sender_type = 'customer')
                                          ← 대화 소유권 검사 없음. 타인 대화에 글 심기 가능
⑥ customer_chat_tokens 에 만료 컬럼 없음 → 토큰 무기한 유효
```
`USING(true)` 는 조건 없이 전부 허용한다는 뜻이다.

### 대조 — 설계사 쪽은 정상이다
```
정책 총 181건 (67개 테이블) · RLS 켜진 테이블 84/84
'본인 것만'(auth.uid()) 방식 정책 106건
익명 무조건 허용 4건  ← 이번 수정 대상
```
`customers`, `conversations`, `conversation_summaries` 등 agent 경로는 전부 `auth.uid()` 로 격리돼 있다. **건드리지 말 것.**

### 실측 스키마
```
customer_chat_tokens : id(uuid) customer_id(uuid) agent_id(uuid) token(text UNIQUE) is_active(bool) created_at
conversations        : id(uuid) customer_id(uuid) agent_id(uuid) last_message_at last_message_preview unread_count created_at
conversation_messages: id(uuid) conversation_id(uuid) sender_type(text) content(text) created_at
```
`token` 은 36자 UUID 형식이고 `UNIQUE` 제약이 있다. **엔트로피는 문제없다.**

## ★ 실사용 실측 — 카나리 불필요
```
conversation_messages 3건 : 전부 2026-04-29 01:20 (30초 사이) = 테스트 1회
이후 약 4개월간 활동 0
customer_chat_tokens 1건 · conversations 1건
반면 최근 30일 신규 고객 6명  ← CRM 은 실사용 중
```
**고객 채팅 링크 기능은 실사용되지 않고 있다.** 따라서 정책 병행(카나리) 없이 전환해도 실서비스가 끊기지 않는다.
단 **CRM(설계사 경로)은 실사용 중**이므로 agent 정책을 건드리면 즉시 사고다.

## 회장 결정
- 토큰 만료: **무기한 유지**(회장 지시). 대신 기존 `is_active` 플래그와 삭제 기능이 회수 수단으로
  실제 동작하는지 확인하고, 동작하지 않으면 그 사실을 보고하라. (`CrmCustomerDetail.tsx:313` 에 delete 존재)

## 설계 (ANU·Codex 합의)
**토큰을 클레임에 담은 커스텀 JWT 방식**을 채택한다.
- 고객이 `/chat/:token` 으로 진입할 때 토큰을 검증하고, 그 토큰을 클레임에 담은 **단기 JWT 를 발급**한다.
- RLS 정책은 `auth.uid()` 대신 **그 클레임**을 참조해 "자기 토큰에 묶인 대화만" 허용한다.

**기각된 대안과 이유**(다시 검토하지 말 것):
- 요청 헤더/GUC 참조 → 위조 표면이 크고 운영 복잡
- Edge Function/service_role 전면 프록시 → SPA 가 PostgREST 를 직접 호출하는 현 구조를 통째로 바꿔야 함

다만 **구현 상세는 착수 전 조사 후 ANU 승인**을 받는다. Supabase 에서 커스텀 클레임을 안전하게 주입하는
경로(Edge Function 발급 / auth hook / 기타)를 조사해 근거와 함께 제안하라.

## 요구사항
1. **④②① 읽기 잠금** — 익명은 자기 토큰에 묶인 대화·메시지·토큰만 조회 가능
2. **⑤ INSERT 소유권 검사** — `sender_type` 뿐 아니라 **대화 소유권**을 검사한다.
   자기 토큰에 묶이지 않은 `conversation_id` 로는 삽입 불가
3. **③ UPDATE 최소화** — 이 정책의 용도는 `unread_count`/`last_message_*` 갱신으로 추정된다.
   **먼저 프론트가 실제로 무엇을 PATCH 하는지 확인**하고, 필요한 범위로만 좁힌다.
   불필요하면 제거하고 서버/트리거로 옮기는 방안을 제안하라
4. **롤백 SQL 을 먼저 작성**한다. 적용 SQL 과 롤백 SQL 을 쌍으로 제출한다

## ★ DDL 적용 절차 (엄수)
- 접속: `psycopg2` + `aws-1-ap-northeast-2.pooler.supabase.com:5432`(session mode).
  user `postgres.zayhfjuwviporbzokudr`, 비밀번호는 `/home/jay/projects/InsuRo/.env` 의 `SUPABASE_DB_PASSWORD`.
  **비밀번호를 로그·보고서·커밋에 절대 남기지 말 것.**
- **적용 SQL 과 롤백 SQL 을 ANU 에게 먼저 제출하고 승인받은 뒤 실행한다.** 무단 적용 금지.
- 적용 전 현재 정책 전량을 `pg_policies` 에서 덤프해 백업 파일로 남긴다.
- 적용 후 즉시 검증하고, 실패 시 **롤백 SQL 을 그 자리에서 실행**한다.

## 검증 (전부 실측 증거)
1. **적용 전 취약점 재현** — anon 키로 타인 대화·메시지 조회가 성공하는 것을 먼저 기록한다(기준선)
2. **적용 후 동일 프로브가 차단**되는 것을 확인한다. 최소 4종:
   - 남의 대화 조회 / 남의 메시지 조회 / 남의 대화 수정 / 남의 대화에 메시지 삽입
3. **정상 경로 무손상** — 유효한 토큰으로 진입한 고객이 자기 대화를 읽고 쓰는 흐름이 동작한다.
   실브라우저로 확인하고 스크린샷 첨부(픽스처 아님을 명시)
4. **★ CRM 무회귀** — 설계사 로그인 후 고객 목록·상담 화면이 정상 동작하는지 확인한다.
   CRM 은 실사용 중이라 여기서 깨지면 즉시 사고다. `auth.uid()` 정책 106건을 건드리지 않았음을 diff 로 증명한다
5. `npm run build` EXIT=0 · `npx vitest run` 회귀 유지

## 범위 · 금지
**허용**: `supabase/migrations/**` · `src/pages/CustomerChat.tsx` · 토큰→JWT 교환 경로(신규) · 관련 테스트

**금지**:
- **agent 경로 정책 106건** — `auth.uid()` 방식 정책은 손대지 말 것
- `src/components/AuthGuard.tsx` · `src/components/AuthForm.tsx` — task-2990(PR #239) 미머지 상태
- `scripts/deploy/**` · `ops/**` — task-2988 영역
- `src/pages/SilsonCalculator.tsx` · `server/silson/**`

## allowed_resources (본 task의 capability)

```yaml
allowed_resources:
  paths:
    - "supabase/migrations/**"
    - "supabase/functions/**"
    - "src/pages/CustomerChat.tsx"
    - "src/integrations/supabase/**"
    - "tests/**"
    - "e2e/**"
    - "memory/reports/task-2991.md"
  forbidden_paths:
    - "src/components/AuthGuard.tsx"
    - "src/components/AuthForm.tsx"
    - "scripts/deploy/**"
    - "ops/**"
    - "server/silson/**"
    - "src/pages/SilsonCalculator.tsx"
    - ".github/**"
  commands:
    - "npm run build"
    - "npx vitest run"
    - "npx playwright test"
    - "python3"
  merge_policy: "tiered"
  ttl_hours: 72
```

## 운영 계약
- `origin/main` 기준 `git pull --ff-only` 후 시작. 워크플로우 `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` · QC `/home/jay/workspace/teams/shared/QC-RULES.md`
- `WORKSPACE_ROOT=/home/jay/workspace` · `CHAT_ID=6937032012` · 수집자 key `ANU_KEY=c119085addb0f8b7`
- 완료 경로는 `finish-task.sh` 실행이 유일하다. 수동 `.done` 금지.

## 완료 조건
설계안 ANU 승인 → SQL 쌍 ANU 승인 → 적용 → 취약점 4종 차단 실증 → 정상 경로·CRM 무회귀 → 회귀 유지

## 보고
**PR 생성까지가 범위다. 머지는 ANU 가 한다. 직접 머지하지 말 것.**
`memory/reports/task-2991.md` 작성 후 표준 완료 콜백 등록. 콜백 프롬프트 **UTF-8 3900 bytes 이하**.

## goal_assertions (auto-generated)
- `npm run build`
- `npx vitest run`
