보안 체크리스트
너홀로프로는 법률 사무소 PII (주민번호·사건번호·채권자 정보) 를 다루므로 보안 실수의 블러스트가 큽니다. 본 문서는 CLAUDE.md 와 각 아키텍처 문서에 흩어진 보안 규칙을 개발자가 PR 전에 훑을 수 있는 단일 체크리스트 로 통합합니다.
1. 핵심 원칙 (절대 규칙)
| 원칙 | 이유 |
|---|---|
| PII 는 Firestore 에 저장 금지 — 원본 OCR 텍스트는 Cloud Storage 전용 | DLP 비식별화 이전의 원본은 Firestore 인덱스 · 백업 · 읽기 권한 확산 위험 |
모든 Gemini 호출은 클라이언트 전용 (firebase/ai) | 사용자 세션 컨텍스트 유지 + 서버 키 노출 방지 |
| reCAPTCHA v3 무료 티어 고정 | Enterprise 업그레이드 = 계약 비용 폭증. 가드 위반 시 chore(release): 롤백 |
| apps/* 간 import 금지 | 독립 배포 단위 — ESLint 가 강제 |
config/appMetadata.features.infraHardening.* 플래그로 보안 기능 감싸기 | 사고 시 Firestore 문서 편집만으로 원격 롤백 |
2. 개발 시 체크리스트 (PR 전 필수)
Server Action
- 파일 상단
"use server"선언 + async function 만 exportinterface/type/const는 별도types/에서import type—__tests__/server-action-integrity.test.ts가 자동 검증
- 첫 줄은 세션 획득:
const session = await requireStaffSession() - mutating action 은 소유권 게이팅:
requireCaseOwnership/requireInvoiceOwnership/requireDocOwnership—{ mutating: true }로 tenant 소유권 + readOnly 동시 검증 - 신뢰 경계 검증 — Zod 스키마 또는
validate*헬퍼로caseId· enum · 문자열 안전성 확인 - catch 블록에
console.error("[domain] action:", err)+toErrorMessage— 원본 스택 로깅 - 반환 타입
ActionResult<T>— 에러 코드PLAN_READ_ONLY/PLAN_LIMIT_EXCEEDED/OWNERSHIP_VIOLATION/CASE_TENANT_MISSING/RECAPTCHA_FAILED
Firestore Rules
- 새 컬렉션 추가 시
firestore.rules에 명시적 allow 규칙 — 기본 deny 유지 - 읽기·쓰기 분리 검증:
allow read/allow create/allow update/allow delete - tenant 경로 하드코딩 금지 — 헬퍼(
isStaffOrOwner(tenantId),belongsToTenant(tenantId)) 재사용 - 에뮬레이터 Rules Playground 로 검증 후 배포 (
firebase deploy --only firestore:rules는 즉시 반영)
PII 마스킹
- AI 에 전달하기 전
maskPII()적용 - 주민번호·전화번호 3가지 포맷 모두 커버 — 하이픈(
123456-1234567), 공백(123456 1234567), 구분자 없음(1234561234567) - 테스트:
ai-helpers.test.ts에 새 포맷 케이스 추가
CSP (외부 도메인)
- 새 외부 URL 호출 시
csp-headers.ts의getCspDirectives()수정 -
apps/web/__tests__/next-config-headers.test.ts에 assertion 추가 -
pnpm e2eCSP violation 스펙 자동 실패 여부 확인
Storage
- 증거·서류 등 PII 포함 파일은
users/{uid}/또는tenants/{tid}/경로 - Signed URL 15분 만료 고정 — 길게 주지 않기
- Storage Rules 50MB 크기 제한 검증
- Content-Type 화이트리스트 (이미지·PDF 만)
세션 · 인증
- 포털 라우트 (
apps/web/app/(portal)/) 는 iron-session 기반 — Firebase Auth 아님 - 일반 사용자는
requireStaffSession(), 포털은requirePortalSession()구분 - 새 공개 페이지 추가 시
apps/web/app/api/proxy.ts의PUBLIC_PATHS에 등록
포털 (4자리 접속 코드)
- 접속 코드 평문 저장 + 10회 오입력 시 자동 잠금 —
accessCodeAttempts필드 증가 로직 유지 - 포털 토큰 발급은 사건 상세의
PortalLinkDialog전용 — 다른 경로에서 발급 금지 - 저장 컬렉션은
portalTokens만 —portalInvites는 #94 에서 전면 제거됨
3. 권한 검증 계층 (방어 심층)
하나의 요청이 처리되기까지 거치는 게이트 — 각 계층이 모두 통과해야 데이터 접근 가능.
1. Edge: apps/web/app/api/proxy.ts
└─ PUBLIC_PATHS 에 없는 경로는 인증 필수
↓
2. Layout: (workspace)/layout.tsx, (portal)/layout.tsx
└─ getServerSession() + Custom Claims (tenantId, role) 추출
↓
3. Server Action: "use server" 최상단
└─ requireStaffSession() / requireOwnership*()
↓
4. Firestore Rules: firestore.rules
└─ isStaffOrOwner(tenantId) 등 최종 검증 (네트워크 레이어)
원칙: 상위 계층에서 막혔다고 하위 계층 생략 금지. 네트워크 우회·Custom Claims 조작 등에 대비해 Firestore Rules 가 최후 방어.
4. PII 라이프사이클
| 단계 | 저장 위치 | PII 처리 |
|---|---|---|
| 사용자 입력 (주민번호·전화번호) | Firestore cases/{id} 등 | 평문 저장 (tenant 격리) |
| AI 호출 직전 | (메모리) | maskPII() 로 마스킹 후 Gemini 전달 |
| AI 응답 저장 | Firestore | 마스킹된 상태로 저장 |
| 증거 업로드 (이미지·PDF) | Cloud Storage | 원본 보관 |
| 사건 종결 → OCR | Cloud Functions legacy-pipeline.ts | Vision OCR → Cloud DLP 비식별화 → Storage 는 원본, Firestore 는 비식별화본만 |
| Vertex AI Search 인덱스 | RAG index | 비식별화된 텍스트만 |
| Signed URL 다운로드 | (응답) | 15분 만료 |
원본 OCR 텍스트 Firestore 금지
DLP 비식별화 이전의 원본은 Firestore 필드 originalText 등에 절대 저장하지 마세요. Storage 파일로만 유지. 이 규칙 위반은 PII 유출 사고의 가장 흔한 원인입니다.
5. 인프라 킬 스위치 (infraHardening.*)
새 보안 기능은 반드시 config/appMetadata.features.infraHardening.* 플래그로 감쌉니다. Firestore 문서 1건 편집만으로 배포 없이 원격 비활성화 가능.
예시 (실제 + 가상):
infraHardening.portalRecaptcha— 포털 reCAPTCHA 요구infraHardening.caseOwnershipGating— mutating action 에 소유권 + readOnly 게이팅infraHardening.storageUrlTtlMinutes— signed URL 만료 분 (기본 15)
사고 대응:
Firebase Console → Firestore
→ config/appMetadata
→ features.infraHardening.<flagName>: true → false
(즉시 반영, 브라우저 재진입 시 적용)
플래그 코드를 작성할 때는 useAppMetadata() (클라) / getAppMetadata() (서버) 로 조회하고, 플래그 OFF 시 기능이 "이전 상태" 로 복귀 하도록 설계. Fail-closed 가 필요한 영역은 OFF 시 deny 가 되도록.
6. 플랜 게이팅과의 관계
- ADR 0002 (2026-04-19 전 사용자 무료) 이후 트라이얼·grace·readOnly 생명주기는 제거됨.
requireOwnership*는 소유권 검증에 집중. - 어뷰징 방어용 한도 (monthly AI assist 등) 는 운영 안정성 목적으로
PlanLimits.registered단일 tier 로 시행. - 구체 소스 경로는
CLAUDE.md§"핵심 제약" 참조.
7. 새 보안 기능 개발 플로우
- 플래그 정의 —
config/appMetadata.features.infraHardening.<name>을 metadata 4곳 + Firestore 에 추가 (CLAUDE.md§"앱 메타데이터" 참조) - 기능 구현 — 플래그 OFF 시 이전 동작, ON 시 새 동작
- 단위·E2E 테스트 — ON / OFF 양쪽 케이스
- 프로덕션 배포 — 플래그 OFF 상태로 먼저 코드 배포
- Firestore 에서 플래그 ON — 작은 tenant 에서 먼저 검증 (현재는 전역 플래그만 있음)
- 모니터링 1주 — Sentry · Firebase Logs 관찰
- 기본값 재고 — 안정적이면 다음 PR 에서 metadata
defaults.ts를 true 로 (단, 기존 Firestore 문서는 영향 없음)
8. 사고 대응 매트릭스
| 사고 유형 | 즉시 (1분) | 단기 (1시간) | 정식 복구 |
|---|---|---|---|
| PII 유출 (공개 경로) | 해당 파일 Storage ACL private | Signed URL 만료 단축 | DLP 재실행 + 감사 로그 검토 |
| 무단 tenant 접근 (IDOR) | 관련 infraHardening.ownershipGating ON | 세션 강제 만료 | requireOwnership* 호출 누락 Server Action 수정 PR |
| Gemini 요금 폭증 | features.ai.* OFF (Firestore 편집) | assistLimit 일괄 축소 | 원인 함수 재설계 + PR |
| CSP bypass / XSS | csp-headers.ts 엄격화 긴급 배포 | Cloudflare WAF 규칙 | 입력 검증 강화 + E2E 추가 |
| Firestore Rules 실수 배포 | 이전 firestore.rules 로 재배포 | — | Rules 에뮬레이터 테스트 필수화 |
| Cloud Function 권한 오남용 | firebase functions:delete | Scheduler 일시정지 | IAM 최소 권한 재검토 + 재배포 |