본문으로 건너뛰기

보안 체크리스트

너홀로프로는 법률 사무소 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 만 export
    • interface / 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.tsgetCspDirectives() 수정
  • apps/web/__tests__/next-config-headers.test.ts 에 assertion 추가
  • pnpm e2e CSP 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.tsPUBLIC_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원본 보관
사건 종결 → OCRCloud Functions legacy-pipeline.tsVision 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. 새 보안 기능 개발 플로우

  1. 플래그 정의config/appMetadata.features.infraHardening.<name> 을 metadata 4곳 + Firestore 에 추가 (CLAUDE.md §"앱 메타데이터" 참조)
  2. 기능 구현 — 플래그 OFF 시 이전 동작, ON 시 새 동작
  3. 단위·E2E 테스트 — ON / OFF 양쪽 케이스
  4. 프로덕션 배포 — 플래그 OFF 상태로 먼저 코드 배포
  5. Firestore 에서 플래그 ON — 작은 tenant 에서 먼저 검증 (현재는 전역 플래그만 있음)
  6. 모니터링 1주 — Sentry · Firebase Logs 관찰
  7. 기본값 재고 — 안정적이면 다음 PR 에서 metadata defaults.ts 를 true 로 (단, 기존 Firestore 문서는 영향 없음)

8. 사고 대응 매트릭스

사고 유형즉시 (1분)단기 (1시간)정식 복구
PII 유출 (공개 경로)해당 파일 Storage ACL privateSigned URL 만료 단축DLP 재실행 + 감사 로그 검토
무단 tenant 접근 (IDOR)관련 infraHardening.ownershipGating ON세션 강제 만료requireOwnership* 호출 누락 Server Action 수정 PR
Gemini 요금 폭증features.ai.* OFF (Firestore 편집)assistLimit 일괄 축소원인 함수 재설계 + PR
CSP bypass / XSScsp-headers.ts 엄격화 긴급 배포Cloudflare WAF 규칙입력 검증 강화 + E2E 추가
Firestore Rules 실수 배포이전 firestore.rules 로 재배포Rules 에뮬레이터 테스트 필수화
Cloud Function 권한 오남용firebase functions:deleteScheduler 일시정지IAM 최소 권한 재검토 + 재배포

9. 교차 참조