# task-2991 마아트 독립 검증 보고서

- 검증관: 마아트 (독립 검증) / 일자: 2026-08-20
- 대상: /home/jay/projects/InsuRo/.worktrees/task-2991-dev3 (task/task-2991-dev3, base 543af90d)
- 원칙: 팀 자기증명 미신뢰. 모든 수치는 본 검증관이 직접 실행/조회하여 재측정.
- 금지 준수: 프로덕션 DB 쓰기/DDL 없음. 파일 미수정(본 보고서 제외). PII/비밀 미출력.

---

## 재측정 결과표

| 항목 | 팀 주장 | 마아트 재측정치 | 판정 |
|---|---|---|---|
| V1. 회귀 수치 | 87 files / 1237 passed / EXIT=0 | npx vitest run 직접 실행 결과: Test Files 87 passed (87) / Tests 1237 passed (1237), exit 0, 19.38s. 기준선(86/1232) 대비 실패 전환 0건 | PASS (완전 일치) |
| V2. 빌드 | EXIT=0 | npm run build 직접 실행 결과: 콘솔 출력 "built in 12.33s" 확인, PWA precache 185 entries 정상 생성, PIPESTATUS로 확인한 실제 종료코드 0 | PASS |
| V3. 롤백 SQL 충실도 | anon 5건 원문 그대로 복원 | 백업 JSON(_backup/task-2991_pg_policies_before.json)의 customer_chat_tokens / conversations(x2) / conversation_messages(x2) anon 정책 5건을 policyname/cmd/roles/qual/with_check 5개 필드 전부 1:1 대조한 결과 ROLLBACK.sql 5건 전부 정확히 일치(문자 단위) | PASS |
| V4. agent 경로 무침범 | auth.uid() 정책 DROP/ALTER/CREATE 0건 | git diff 543af90d..HEAD 에서 실제 SQL 문(DROP POLICY/CREATE POLICY/ALTER)을 전수 추출한 결과 대상은 "Anon can ..." 5개뿐(적용 SQL 5건 DROP, 롤백 SQL 5건 DROP+CREATE). auth.uid() 문자열이 등장하는 곳은 전부 주석 또는 백업 JSON 파일(추가된 참조 데이터)뿐, 실행 SQL 문 안에는 0건 | PASS |
| V5. SECURITY DEFINER 안전성 | 전부(5개 함수) search_path + REVOKE+GRANT 적용 | grep 직접 실행 결과 SET search_path = public, pg_temp 는 5/5(RPC 4종 + 트리거함수 전부). REVOKE ALL ON FUNCTION 은 4건, GRANT EXECUTE ... TO anon 은 4건 — RPC 4종에는 있으나 트리거함수 chat_update_conversation_on_message()에는 없음 | PASS(단서) — 팀 sql-pair 보고서의 "전부 REVOKE/GRANT" 표현은 부정확(실제 4/5). 트리거함수는 반환형이 trigger 라 Postgres가 일반 SQL 호출을 원천 차단(표준 동작)하므로 실질 익스플로잇 경로는 없음으로 판단되나, 보고서 서술 정정 필요 |
| V6. 프로덕션 미적용 확인 | 여전히 취약(미적용) | anon 키로 non-destructive 프로브 3종 직접 실행 결과 conversations 200/0-0/1, conversation_messages 206/0-0/3, customer_chat_tokens 200/0-0/1. 전부 200/206 + content-range 로 행 노출 확인 = 차단되지 않음 | PASS (팀 주장대로 미적용 확인. 무단 적용 없음) |
| V7-정정1. auth.uid() "106건 아님" | 문자열포함145 / 직접등치117 / roles=authenticated80 / roles=public65 | 백업 JSON 181건 직접 집계 결과 문자열포함 145건 일치, roles=authenticated 80 / roles=public 65 (145건 부분집합 내 정확히 일치, 80+65=145). "직접 등치" 재현 시도(정규식 2종)는 101~102건으로 나와 팀 주장 117건과 불일치(재현 실패) | 부분 PASS / 불일치 1건 — 핵심 결론("106은 어떤 기준으로도 안 나온다")은 145 · 80 · 65 수치로 재확인됨. "직접 등치 117"은 재측정에서 재현 안 됨(101~102) — 채택치는 101~102 |
| V7-정정2. anon customers/profiles 0행 | 0행 | anon 프로브 직접 실행 결과 customers 200/*/0, profiles 200/*/0 — 0행 확인 | PASS |
| V7-정정3. 20260309160000 마이그레이션이 토큰 SELECT 강화 안 함 | 강화 안 함(참조만) | 파일 전문 직접 읽은 결과 Part 3(RLS 하드닝)은 conversation_messages(SELECT/INSERT), conversations(SELECT/UPDATE), push_subscriptions(x3)에만 DROP POLICY + CREATE POLICY 적용. customer_chat_tokens 자체의 SELECT 정책은 이 파일에서 단 한 번도 DROP/CREATE 되지 않음(다른 정책 안에서 JOIN 참조만 됨) — USING(true) 그대로 방치 확인. 바인딩이 customer_id 기준(토큰값 비교 없음)인 것도 확인 | PASS |
| V8. 범위 준수 | 자진신고 1건(CustomerChat.gateNotice.test.tsx) 외 범위 내 | git diff --name-only 543af90d..HEAD 8개 파일을 allowed_resources.paths glob과 코드로 전수 매칭한 결과 7개 ALLOWED, src/pages/__tests__/CustomerChat.gateNotice.test.tsx 1건 OUT OF SCOPE 확인(tests/** 는 최상위 tests/ 디렉터리만 매치, src/pages/__tests__/ 는 미포함; src/pages/CustomerChat.tsx 는 파일명 완전일치만 허용). forbidden_paths 침범은 0건 | 범위 위반 1건 확인(팀 자진신고와 일치, 독립 재확인 완료) |
| V9. PII 누출 | 없음 | diff 전체 + 보고서 6종에 전화번호/주민번호/API키 패턴 grep 결과 매치 1건은 테스트 코드의 더미 전화번호 010-1234-5678(플레이스홀더, 실제 PII 아님) 뿐. SUPABASE_SERVICE_ROLE_KEY 언급은 환경변수 이름만(값 아님). RPC 4종 반환값 직접 확인 결과 chat_gate_info 는 agent_display_name 만, chat_verify_and_open/chat_send_message 는 uuid 만, chat_list_messages 는 메시지 내용만 반환하며 customers.name/phone, profiles(display_name 제외) 직접 반환 0건 | PASS |

---

## 불일치 목록

1. V5 - REVOKE/GRANT 커버리지: 팀 sql-pair 보고서(task-2991-sql-pair.md 29행)는 "전부 REVOKE ALL ... FROM PUBLIC 후 GRANT EXECUTE ... TO anon"이라고 서술했으나, 실측 결과 트리거함수 chat_update_conversation_on_message()에는 REVOKE/GRANT가 없음(4/5). search_path는 5/5로 정확. 실질 위험은 낮음(트리거함수는 Postgres가 직접 SQL 호출을 구조적으로 차단)이나 보고서 서술은 부정확 — 채택치: search_path 5/5, REVOKE/GRANT 4/5.
2. V7-정정1 - "직접 등치 117건": 재현 시도(정규식 2종: 단순 컬럼 등치, 테이블한정/캐스트 포함 등치)로 101~102건만 나옴. 팀 수치(117)와 16건 차이로 재현되지 않음. 다만 이 하위 수치는 "106이 아니다"라는 핵심 결론의 보조 근거일 뿐이며, 그 결론 자체는 문자열포함(145) · roles 분해(80+65=145) 수치로 별도 확증됨 — 채택치: 101~102(117 아님, 근거 불일치이나 결론에는 영향 없음).
3. V8 - 범위 외 파일: 팀이 이미 task-2991-frontend.md에서 자진신고했고, 본 검증관이 독립적으로 glob 매칭 코드를 돌려 동일하게 재확인함(src/pages/__tests__/CustomerChat.gateNotice.test.tsx). "불일치"는 아니고 팀 보고와 일치하나, ANU 승인 없이는 머지 불가한 항목이므로 목록에 유지.

---

## 종합 판정

PR 생성 준비는 되어 있으나, ANU 승인 없이 머지는 불가. 근거:

- 안전성 핵심 항목(V3 · V4 · V5 · V6 · V9)은 전부 재측정으로 확증됨. 롤백 SQL은 백업 원문과 필드 단위로 완전 일치하고, agent(auth.uid()) 경로는 실행 SQL 문 어디에도 손대지 않았으며, SECURITY DEFINER 함수는 search_path 하이재킹 방지가 5/5 적용되어 있고, 프로덕션은 실제로 아직 미적용 상태(무단 배포 없음)이며, PII 노출도 없다.
- 테스트/빌드(V1 · V2)는 팀 보고와 완전히 일치하는 수치로 직접 재현됨.
- 명세 정정 3건(V7)은 핵심 결론이 전부 재확인됨 — "106건"은 어떤 방식으로도 재현 불가, anon의 customers/profiles 접근은 실제로 0행, 기존 미적용 마이그레이션은 토큰 테이블을 강화하지 않음이 파일 직독으로 확인됨. 다만 보조 수치(직접 등치 117건)는 재현되지 않아 이 하나만 미확정으로 남긴다.
- 범위 위반이 실제로 1건 존재한다(src/pages/__tests__/CustomerChat.gateNotice.test.tsx, allowed_resources.paths 밖). 팀이 자진신고했고 내용은 기존 회귀 테스트의 모킹 갱신뿐으로 위험도는 낮으나, dispatch.py/task-scope-guard.sh 체계상 ANU가 명시적으로 범위 확장을 승인해야 머지 가능한 사안이다.
- DDL 미적용 확인(V6)이 재측정으로 성립하므로, 이번 PR은 "적용 SQL · 롤백 SQL 쌍 + RPC 전환 코드"만 담고 있고 프로덕션에는 아직 아무 영향이 없다 — PR 자체를 여는 행위는 안전하다.

결론: PR 생성은 가능. 단, 머지 전 ANU가 (1) 범위 외 테스트 파일 1건 승인 여부, (2) chat_update_conversation_on_message REVOKE 누락에 대한 보고서 정정 요구 여부, (3) DDL 실제 적용 시점/방법(회장 승인 게이트)을 판단해야 한다. 팀이 요청한 "ANU 판단 요청 4건"(설계 경로 승인 / 검증요건 재정의 / DDL 적용 승인 / 범위 외 발견)은 본 재측정으로 사실관계가 뒷받침됨을 확인했다.
