Skip to content

도메인: Firebase App Check (앱 무결성 검증)

왜 필요한가

위협 모델

공시한입의 LLM 호출은 공시당 1회 (스케줄러가 분석 시) — 사용자 reveal 은 캐시된 결과 DB 조회라 호출당 LLM 비용이 추가로 발생하지 않는다. 그래서 위협은 "compute 비용 폭증" 이 아니라 수익 funnel 우회 + 다중 계정 어뷰즈 다.

  • ad_free_pass IAP / 광고 시청 funnel 우회 (cap 무력화)
  • 다중 계정 어뷰저 / 봇이 정상 분포를 흐려 정책 시그널 왜곡
  • AdMob SSV legacy claim 경로 우회 (/quota/ad-reward 직접 호출)
공격방법App Check 없을 때App Check 있을 때
JWT 탈취 + 직접 API 호출JWT 추출 후 /quota/ad-reward 에 반복 호출quota 무한 증가 (used_count 무한 음수)prod 에서 404 (경로 차단), SSV 만 허용
앱 리패키징AdMob SDK 제거 + 커스텀 앱 빌드 후 배포SSV 콜백 없이 클라이언트 claim 가능변조 앱 서명 불일치 → token 발급 불가
루팅/탈옥 기기Play Integrity/AppAttest 우회 후 앱 수정reveal 무제한strict 모드: 403
에뮬레이터 자동화가상 기기 대량 생성계정당 월 200 × N 계정Play Integrity MEETS_BASIC_INTEGRITY 실패

AdMob SSV만으로 커버되지 않는 부분: SSV는 Google이 콜백을 직접 보내므로 클라이언트 위변조와 무관하다. 하지만 /quota/ad-reward legacy path와 reveal 자체가 뚫려있으면 광고를 안 봐도 quota를 늘릴 수 있다.


동작 원리

전체 플로우

mermaid
sequenceDiagram
    participant App as Flutter 앱
    participant Firebase as Firebase
    participant BE as dartbrief 백엔드

    Note over App: 1. 앱 시작 시 App Check 초기화<br/>Android: Play Integrity API<br/>iOS: AppAttest<br/>→ 디바이스 + 앱 서명 검증

    Note over App: 2. reveal 요청 직전<br/>getToken() 호출
    App->>Firebase: 토큰 요청
    Firebase-->>App: 서명된 JWT (1시간 TTL, SDK가 자동 캐시)

    App->>BE: 3. POST /ai/analyze/{id}/reveal<br/>Authorization: Bearer $JWT<br/>X-Firebase-AppCheck: $appCheckToken

    BE->>Firebase: 4. App Check 토큰 검증<br/>JWK endpoint에서 공개키 fetch (6h 캐시)<br/>RS256 서명 검증 / iss·aud 클레임 확인
    Firebase-->>BE: 공개키 응답

    Note over BE: 5. 검증 통과 → reveal 처리
    BE-->>App: 응답

토큰 구조 (Firebase App Check JWT)

Header: { "alg": "RS256", "kid": "<key_id>", "typ": "JWT" }

Payload:
{
  "iss": "https://firebaseappcheck.googleapis.com/532121942538",
  "aud": ["projects/532121942538", "projects/dartbrief"],
  "sub": "1:532121942538:android:5651b10c890382b7b4bdf3",  // App ID
  "exp": <unix_timestamp>,
  "iat": <unix_timestamp>
}

백엔드 검증 체크리스트:

  1. typ = "JWT" (헤더)
  2. RS256 서명 → Firebase JWK 공개키로 검증
  3. exp 만료 여부
  4. iss = "https://firebaseappcheck.googleapis.com/532121942538"
  5. aud"projects/532121942538" 또는 "projects/dartbrief" 포함
  6. sub (App ID) ∈ FIREBASE_ALLOWED_APP_IDS allowlist (strict 모드)

플랫폼별 Provider

플랫폼Provider검증 내용OS 요구사항
Android (prod)Play Integrity앱 서명 + 기기 무결성 + Google Play 설치 여부Android 5.0+
iOS (prod)AppAttest앱 서명 + 정품 Apple 기기iOS 14+
Dev (양 플랫폼)Debug항상 토큰 발급 (개발용 DebugToken)제한 없음

App Check가 막는 것:

  • 리패키징된 앱 (서명 불일치)
  • 루팅된 기기 (Play Integrity MEETS_BASIC_INTEGRITY 실패)
  • 에뮬레이터 (Play Integrity MEETS_DEVICE_INTEGRITY 실패)
  • AppAttest 우회 시도

App Check가 막지 못하는 것:

  • 기기에서 직접 트래픽 스니핑 (HTTPS 레벨 공격은 별도 SSL Pinning 필요)
  • 탈옥 기기 중 AppAttest 우회 성공 케이스 (iOS 제한적)

구현 상태

보호 대상 엔드포인트 (opt-out 모델, 2026-05-09 PR #59)

App Check 는 글로벌 미들웨어 체인 노드로 한 번만 끼워져 있어 모든 엔드포인트가 기본 보호된다. 새 보호 라우트 추가 시 wrap 누락 위험 X. 면제는 아래 매트릭스만:

면제 트리거조건이유
/actuator/*모든 메서드외부 모니터링이 호출, 클라 토큰 없음
/api/v1/admob/ssv모든 메서드Google AdMob 서버→서버 (ECDSA 서명으로 별도 검증)
/api/v1/admin/*모든 메서드admin 토큰으로 별도 인증
X-Worker-Secret 헤더 매칭GET 만웹 (CF Workers SSR) 의 대안 ACL — AppCheck 토큰 발급 불가능한 SSR 컨텍스트

경로 자체로는 더 이상 면제 X — 모든 라우트가 기본 보호. 웹은 Worker shared secret 헤더로 인증 (모바일은 AppCheck 토큰, 웹은 secret — 둘 중 하나만 통과). secret 가 미설정 (WEB_SECRET="") 이면 헤더 검사 자체 비활성 (dev local 안전망).

웹 ACL 배경 (2026-05-10 PR #67): Next.js + CF Workers 배포 후 Firebase App Check 가 웹 fetch 를 막아서 SSR/ISR 응답 모두 403. 웹은 Play Integrity / AppAttest 둘 다 발급 불가 (모바일 전용). 단순 경로 면제는 어뷰저가 같은 path 를 외부에서 호출 시 무방비 → Worker shared secret 으로 caller 식별. 어뷰즈 ROI 추가 차단:

  • lazy auth — 비로그인 시 result 필드 omit (한줄요약만)
  • HTTPCache TTL (30s ~ 10m) — 같은 URL 재호출 비용 0
  • LLM 호출 0 (스케줄러가 사전 분석, reveal 은 캐시 조회만)
  • secret 유출 시 손상 한정 — GET 만 면제, write 작업은 모바일 AppCheck 만 통과

Rate limit: Worker secret GET 트래픽은 일반 IP rate limit (RPS 2 / Burst 10) 우회 → SharedBucketRateLimit (RPS 50 / Burst 200) 단일 공유 버킷 적용. Registry 의 Anonymous helper 가 ctx.IsWorker 보고 자동 분기. CF Workers SSR subrequest 가 단일 IP 로 수렴하는 환경에서 cache miss 동시 발생 시 정상 사용자 429 폭주 방지.

추가로 핸들러 레벨 정책:

  • POST /api/v1/quota/ad-reward — APP_ENV 체크 (prod → 404, SSV 만 허용)

Enforcement 모드

Soft (현재, APP_CHECK_ENFORCEMENT=soft):

  • 토큰 없음 → 로그 + 통과
  • 토큰 있음 + 검증 실패 → 로그 + 통과
  • 클라이언트 배포 중 호환성 유지 목적

Strict (APP_CHECK_ENFORCEMENT=strict):

  • 토큰 없음 → 403 {"status":"FORBIDDEN","message":"App Check token required"}
  • 토큰 있음 + 검증 실패 → 403 {"status":"FORBIDDEN","message":"App Check token invalid"}

Strict 전환 절차

  1. 새 클라이언트 (App Check 포함) 앱스토어 배포 완료
  2. 구 클라이언트 사용률 충분히 낮아진 시점 확인 (Railway 로그 기준)
  3. Railway 환경변수 FIREBASE_ALLOWED_APP_IDS 설정 (Android prod/dev, iOS prod/dev App ID 콤마 구분)
  4. Railway 환경변수 APP_CHECK_ENFORCEMENT=strict 변경
  5. 재배포 (코드 변경 없음)

백엔드 핵심 파일

파일역할
backend/internal/appcheck/verifier.goJWK fetch (6h TTL 캐시, 실패 시 1분 backoff) + RS256 JWT 검증
backend/internal/appcheck/middleware.goMiddleware(v, mode, next) — soft/strict 모드 분기. Registry helper 가 카테고리별로 wrap
backend/pkg/route/registry.goAuthenticated / Anonymous / AuthIssue helper 가 AppCheck wrap. 다른 helper (Admin / AdMobSSV / Actuator) 는 면제
backend/internal/ai/handler.goadReward — non-local/dev 환경 404

Flutter 핵심 파일

파일역할
mobile/lib/firebase_options.dartFirebase 프로젝트 연결 설정
mobile/lib/app/bootstrap.dartFirebase 초기화 + App Check activate
mobile/lib/shared/api/api_client.dartprod 요청마다 X-Firebase-AppCheck 헤더 첨부

Edge Cases

토큰 발급 실패 시 (soft 모드)

getToken() 실패 (네트워크 없음, Firebase 장애 등) → on Exception catch (_) 로 무시 → 헤더 없이 요청 전송 → 백엔드 soft 모드에서 로그 + 통과.

사용자는 reveal이 정상 동작함. strict 모드로 전환 후엔 실패 → 403 → 앱에서 에러 처리 필요 (미구현, 추후 대응).

Firebase JWK 서버 장애 시

백엔드 캐시가 만료되지 않았으면 stale 키로 계속 검증. 캐시 만료 후 JWK 서버도 복구 전이면:

  • stale 캐시에 키가 있으면 stale 키로 검증 (서명 유효하면 통과)
  • stale 캐시에도 없으면 에러 → soft: 로그 + 통과 / strict: 503보다 안전하게 soft fallback 고려 필요

Dev 빌드에서 실기기 테스트

Debug provider는 GOOGLE_APP_CHECK_DEBUG_TOKEN 환경변수 또는 앱 로그에 출력된 debug token을 Firebase Console에 등록해야 한다. 미등록 시 debug provider도 token 발급 실패.

→ Firebase Console → App Check → 앱 선택 → Debug tokens → 추가

App Check 토큰 재사용 (alreadyConsumed)

Firebase App Check 토큰은 기본적으로 재사용 가능 (1시간 TTL). 단, exchangeAppCheckToken 엔드포인트 호출 시 1회용으로 소모 가능하나 현재 미사용. reveal은 멱등성을 DB (user_revealed_disclosures)로 보장하므로 토큰 재사용 허용해도 무방.


환경변수 요약

변수기본값설명
FIREBASE_PROJECT_NUMBER532121942538JWT iss/aud 검증용 프로젝트 번호
APP_CHECK_ENFORCEMENTsoftsoft 또는 strict
FIREBASE_ALLOWED_APP_IDS(없으면 전체 허용)허용할 Firebase App ID 콤마 구분 목록. strict 전환 전 반드시 설정.
APP_ENV(없으면 "" — 차단)local = ad reward 허용. 미설정 시 adReward local 기본값 없이 fail-closed.

관련 문서


법적 페이지 영향

Firebase App Check 는 앱 실행 시점에 디바이스 무결성 토큰 (Android Play Integrity / iOS App Attest) 을 발급받아 Google 서버와 통신한다. 영구 저장 X, 어뷰즈 차단 용도 한정이지만 "자동 수집되는 정보" 로 분류되어 다음 페이지에 명시 필수:

  • frontend/src/app/privacy/page.tsx — 제2조 자동 수집, 제5조 위탁, 제6조 국외 이전
  • frontend/src/app/terms/page.tsx — 제9조·제12조 외부 서비스 목록

웹은 App Check 미적용 (대신 X-Worker-Secret 으로 Worker SSR ACL — docs/operations/cloudflare-deploy.md 참조).