# task-2996 — InsuRo 접근제어 결함 3건 (UI 전수점검 후속)

**레벨**: Lv.2 (보안) · **팀**: dev3-team (다그다) · **level=security** · **회장 승인 완료** (2026-08-21)

## 배경
task-2994(UI 전수 점검)에서 범위 밖으로 발견된 접근제어 결함 3건이다.
**ANU 가 프로덕션 DB·소스에서 직접 재측정**해 봇 보고를 검증했고, 그 결과를 아래에 반영했다.
심각도를 봇 보고보다 **낮춘 항목이 있으니 주의**해서 읽어라.

---

## A-33 — 관리자 라우트 가드가 라우터 계층에 없다 (구조적)

**ANU 실측 (2026-08-21)**
```
src/config/routes.ts   adminOnly: true 라우트 = 13건
src/App.tsx            adminOnly 참조 = 0건    ← 라우터가 이 필드를 읽지 않는다
13개 라우트의 컴포넌트  자체 권한체크(useIsAdmin/isSystemAdmin) 누락 = 0건
```

**즉 현재 실피해는 없다.** 13개 페이지가 각자 자체 체크를 갖고 있어 막힌다.
문제는 **방어가 라우터가 아니라 개별 페이지에 흩어져 있다**는 것이다.
신규 관리자 페이지에서 체크를 한 줄 빠뜨리면 즉시 노출된다. `adminOnly` 필드는 선언만 되고 아무도 안 읽는다.

**추가 실측 — 메뉴 노출**
```
src/components/navigation/ContextSubMenu.tsx:28-57
  pathname.startsWith("/admin") 이면 권한 확인 없이 관리자 메뉴 12개를 렌더
```
비관리자가 `/admin` 경로에 진입하면 **메뉴 목록이 보인다.** 클릭해도 페이지는 막히지만
관리 기능의 존재와 구조가 드러난다.

**요구사항**
1. `App.tsx` 에 **라우터 단일 지점 가드**를 둔다. `adminOnly: true` 인 라우트는 라우터가 차단한다.
   개별 페이지 체크에 의존하지 않는 구조로 바꾼다.
2. `ContextSubMenu` 의 관리자 분기에 권한 조건을 건다.
3. **기존 13개 페이지의 자체 체크는 제거하지 마라.** 이중 방어로 남긴다.
   (제거하면 가드에 구멍이 났을 때 무방비가 된다)

---

## A-40 — `ai_config` 가 전체 인증 사용자에게 SELECT 개방

**ANU 프로덕션 DB 실측**
```
정책 2건:
  SELECT {authenticated}  "Anyone authenticated can read"  USING(true)      ← 결함
  ALL    {authenticated}  "System admins can manage"       USING(has_role(...'system_admin'))
컬럼: api_key 존재
행 수: 1 · provider=lovable · api_key 실제 길이 = 0 (빈값)
```

**★ 심각도 정정**: 봇은 "평문 API 키가 전체 인증 사용자에게 개방"이라 보고했으나,
**실제 저장된 키가 없다(빈값).** 지금 유출되는 키는 0건이다.
구조적 결함은 사실이나 **P0 가 아니다.** 이 사실을 전제로 작업하라.

**요구사항**
- `SELECT {authenticated} USING(true)` 를 제거하고 **관리자만 읽도록** 좁힌다.
- 앱이 이 테이블을 읽는 경로가 있으면 깨진다. **먼저 소비자를 확인**하고, 일반 사용자가 읽어야 하는
  비민감 컬럼이 있다면 **뷰 또는 컬럼 단위 분리**를 제안하라. 무작정 잠그지 말 것.
- DDL 은 **적용 SQL·롤백 SQL 쌍을 ANU 에 먼저 제출하고 승인받은 뒤** 실행한다.

---

## A-39 — `push_subscriptions` anon 쓰기에 소유권 검사 없음

**ANU 프로덕션 DB 실측**
```
anon 쓰기 정책 2건 (전부 이 테이블):
  INSERT {anon}  CHECK(subscriber_type = 'customer')
  UPDATE {anon}  CHECK(subscriber_type = 'customer')
```

`subscriber_type` 만 검사하고 **누구의 구독인지 확인하지 않는다.**
2026-08-21 오전 task-2991 에서 고친 `conversation_messages` INSERT 와 **동일 유형**이다.
익명이 타인의 푸시 구독을 만들거나 덮어쓸 수 있다.

**요구사항**
- task-2991 에서 도입한 **토큰 검증 RPC 방식과 같은 패턴**으로 좁힌다.
  (`chat_gate_info` / `chat_verify_and_open` 등 참고 — `supabase/migrations/` 의 task-2991 마이그레이션)
- **고객 푸시 알림이 끊기면 안 된다.** 명함 고객이 알림을 받는 정상 경로를 보존하라.
- DDL 은 A-40 과 함께 **SQL 쌍 제출 → ANU 승인 → 적용** 순서를 지킨다.

---

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

## 검증
1. **적용 전 기준선 기록** — A-40/A-39 각각 현재 상태를 프로브로 재현해 남긴다.
2. **적용 후 차단 실증** — 같은 프로브가 막히는 것을 확인한다.
   ★ PostgREST 는 RLS 필터 시 403 이 아니라 **빈 결과 + 200/204** 를 반환한다.
   응답 코드만 보고 "관통"으로 오판하지 말고 **DB 에서 실제 데이터 변화를 직접 확인**하라.
   (2026-08-21 ANU 가 이 함정에 걸릴 뻔한 실사례)
3. **정상 경로 무손상** — 고객 푸시 구독이 여전히 동작하는지 확인.
4. **CRM 무회귀** — `auth.uid()` 방식 정책 106건을 건드리지 않았음을 diff 로 증명.
5. **A-33 우회 프로브** — 비관리자 세션으로 `adminOnly` 라우트 직접 접근 시 차단되는지,
   `/admin` 진입 시 메뉴가 노출되지 않는지 확인.
6. `npm run build` EXIT=0 · `npx vitest run` 회귀 유지(base 재측정 기준선 사용. 명세 수치 아님).

## 범위 · 금지
**수정 허용**: `src/App.tsx` · `src/components/navigation/ContextSubMenu.tsx` · `supabase/migrations/**` · 관련 테스트

**금지**:
- **13개 관리자 페이지의 기존 자체 체크 제거** (이중 방어 유지)
- `auth.uid()` 방식 정책 106건 (CRM 실사용 중)
- `src/pages/Privacy.tsx` · `src/config/legalEntity.ts` — task-2995(PR #242) 미머지 상태
- `src/components/AuthGuard.tsx` · `AuthForm.tsx` — task-2990 결과물, 이번 범위 아님

## allowed_resources (본 task의 capability)

```yaml
allowed_resources:
  paths:
    - "src/App.tsx"
    - "src/components/navigation/ContextSubMenu.tsx"
    - "src/components/__tests__/**"
    - "supabase/migrations/**"
    - "tests/**"
    - "e2e/**"
    - "memory/reports/task-2996.md"
  forbidden_paths:
    - "src/pages/Privacy.tsx"
    - "src/config/legalEntity.ts"
    - "src/components/AuthGuard.tsx"
    - "src/components/AuthForm.tsx"
    - "server/**"
    - "ops/**"
    - "scripts/**"
    - ".github/**"
  commands:
    - "npm run build"
    - "npx vitest run"
    - "npx playwright test"
    - "python3"
  merge_policy: "tiered"
  ttl_hours: 72
```

## 운영 계약
- `origin/main` 기준 `git pull --ff-only` 후 시작
- ★ `gh` 호출 시 `GH_TOKEN="$BOT_GITHUB_TOKEN"` 주입 필수 (회장 개인 PAT 사용 금지 — 감사기록 오염)
- 워크플로우 `/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 에 보고하라.** (task-2995 에서 이 방식이 옳았다)

## 완료 조건
A-33 라우터 가드 + 메뉴 조건 · A-40/A-39 SQL 쌍 ANU 승인 후 적용 · 차단 실증 · 정상경로 무손상 · CRM 무회귀 · 회귀 유지

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

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