Financial Integrity Gap Analysis — Wave 4 F0
Reading guide
This is a read-only F0 risk inventory of refunds, settlement, reconciliation, ledger, and audit paths at the stated baseline. Findings are not proof that a fix was implemented.
Prioritize the P0 failure paths and verify each against the latest source and independent fault-injection tests before closing it.
Status: IMPLEMENTED (analysis complete) Baseline:
e37614bonfeat/poc-secure-browser-licensing-rea(PR #5) Method: independent read-only audit ofbe-cloudflare/source, migrations, tests, and CI — existing documentation claims were not trusted; every finding cites file:line evidence. Severity: P0 = money can be lost/duplicated silently or verification lies about coverage; P1 = integrity gap with a plausible trigger; P2 = robustness/observability gap.
1. Refund path — refund_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| REFUND-1 | refund_attempts INSERT가 PG 호출 전에 실행되나 let _ =으로 결과를 버림 | refund_impl.rs:102-105 | INSERT 실패 시 PG 취소는 실행되고 내부 시도 레코드는 없음 → SUM(succeeded) 과소집계 → 추가 환불 가능, 감사 증적 소실 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-2 | 파싱 실패한 요청 본문이 RefundIn::default() → 잔액 전액 환불 | refund_impl.rs:45-48 | 부분 환불 의도가 잘못된 JSON으로 전액 환불+권한 회수로 변함. 400 대신 실행 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-3 | Toss cancel Ok(v) 시 status/cancelAmount 검증 없이 전액 효과 적용 | refund_impl.rs:125-130 | PG 도메인 상태가 아닌 전송 성공만 신뢰 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-6 | 환불 멱등키 없음 — 재시도가 추가 환불을 실행 | refund_impl.rs:7-13, migration 012:14-26 | 부분 환불 재시도가 의도보다 많은 금액 환불. checkout의 idempotency_key 패턴 부재 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-7 | 진행 중 환불 동시성 보호 없음 — cap은 succeeded만 집계 | refund_impl.rs:52,85 | 라이선스 없는 주문(기부 등)은 결정적 id가 없어 이중 전액 환불 가능 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-8 | Toss cancel 요청에 Idempotency-Key 헤더 미첨부 | payment_provider.rs:161-193, refund_impl.rs:124 | 전송 재시도 시 PG측 이중 취소를 금액 cap으로만 흡수 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-9 | PG 성공 + 효과 배치 실패 시 시도가 attempted에 고착 — 재구동 경로 없음 | refund_impl.rs:124-143,311, reconcile_attempts는 needs_review만 조회(:156) | 외부 취소는 실제 발생했으나 내부 paid+활성 라이선스 유지 → 어떤 검사도 탐지 못함. 다음 환불이 remaining을 과대평가해 재취소 시도 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-10 | provider_ref(cancel transactionKey)가 실패한 배치 안에서만 기록됨 | refund_impl.rs:126-128,244-246,193 | 배치 실패 시 외부 취소 참조 유실 → 분쟁/지원 불가 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-11 | reconcile_attempts 내부 let _ = apply_refund_effects(...) | refund_impl.rs:192-194 | 금융 효과 오류 무시 후 진행. 시도는 needs_review 유지로 수렴하나 covered 어트리뷰션 오류 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-12 | PG 취소 매칭이 개별 transactionKey가 아닌 누적 금액 기준 | refund_impl.rs:176-200 | 수동 PG 취소가 내부 시도를 잘못 확정. 어느 시도가 성공인지 특정 불가 시 NEEDS_REVIEW 유지 필요 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| REFUND-13 | 웹훅 CANCELED/PARTIAL_CANCELED 분기 let _ = apply_provider_cancel 후 무조건 processed 마킹 | toss_impl.rs:593,610,625 | 내부 적용 실패 시 이벤트가 processed로 기록되어 재전송도 중복 처리됨 → 영구 드리프트 | P0 | BACKEND_INFRA | NOT_VERIFIED |
2. Inbox / Webhook — toss_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| INBOX-1 | inbox_claim DB 실패 시 Fresh로 진행 (문서화된 fail-open) | toss_impl.rs:399-417 | 하위 멱등성에만 의존. apply_provider_cancel은 호출마다 새 ra_* 발급 → 재처리마다 새 attempt/audit 행 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| INBOX-2 | inbox_mark 오류 let _ = | toss_impl.rs:451-461 | 실패 시 processing 고착 → reconciliation 자동수정으로 수렴. 저위험이나 fail-open | P2 | BACKEND_INFRA | NOT_VERIFIED |
| INBOX-3 | 실패도 200 응답 → Toss 재전송 유도 없음 | toss_impl.rs:466-483,577-583 | failed 행의 자동 redrive 루프 부재 — 수동 복구 의존 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| INBOX-4 | 가드가 no-op이어도 applied:{status} 감사 기록 | toss_impl.rs:624 | 포렌지 기록이 실제 적용 여부를 과장 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| (보존) | 서명 검증, payload_sha256 기반 중복/충돌 감지, authority 재조회 | toss_impl.rs:475-569, payment_provider.rs:136-146 | 이미 안전 — 테스트 검증됨 | — | — | VERIFIED |
3. Settlement — settle_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| SETTLE-1 | 주문 점유가 90건 청크별 복수 db.batch — 원자성 없음 | settle_impl.rs:60-75 | 청크 커밋 후 중단 시 orders.settlement_id가 존재하지 않는 settlement을 참조 → 영구 정산 불가. 탐지 검사도 없음 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| SETTLE-2 | 점유 해제 UPDATE가 let _ = | settle_impl.rs:76-80 | 해제 실패 시 SETTLE-1과 동일한 고아 상태 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| SETTLE-3 | 점유 조건에 status 필터 누락 — 스냅샷과 점유 사이 환불이 반영 안 됨 | settle_impl.rs:30 vs 62 | 환불된 주문이 정산에 포함. refunded_krw 부산물 사용 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| SETTLE-4 | 실행 ID/lease/커서 없음 — 재개 불가 | settle_impl.rs:49-53 | 재시도가 새 set_id를 만들어 부분 점유만 남음 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| SETTLE-7 | settlements.status 전이 없음 — pending 영구, 지급 확정 개념 없음 | settle_impl.rs:91, migration 008:36 (default 'paid') | 정산 생성 ≠ 지급 확정 구분 불가. 전이 경로 부재 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| SETTLE-6 | 수수료율 f64 저장, 미기록 시 조용한 기본값(3.2%/5%), 버전 스냅샷 없음 | settle_impl.rs:37-47 | 요율 변경 전후 거래 재계산 불가 | P2 | BACKEND_INFRA | NOT_VERIFIED |
4. Reconciliation / Verification — reconciliation_impl.rs, verify_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| RECON-1 | q_all()이 모든 D1 오류를 빈 결과로 변환 | reconciliation_impl.rs:55-60 | DB 장애 중 실행 시 completed+findings 0 — 검증 불가능한 순간에 '정상' 보고 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| RECON-2 | 실행이 무조건 completed 마킹 — per-check 오류 상태 없음 | reconciliation_impl.rs:238,272-281 | INCOMPLETE/ERROR가 ok로 보고됨 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| RECON-3 | LIMIT 100/200 + 실행 완전성 회계 없음(스캔 수/커서/재개 없음) | reconciliation_impl.rs:95-222 | 드리프트가 LIMIT 초과 시 초과분 영구 미탐지, completed로 보고 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| RECON-4 | 금전 검사 전무 — 7개 검사 모두 entitlement/queue 구조 검사 | reconciliation_impl.rs:82-225 | capture/refund/net/settlement 정합성을 검증하는 것이 없음 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| RECON-5 | finding fingerprint/수명주기 없음 — 매 실행 신규 ID, 해결 경로 없음 | reconciliation_impl.rs:226-285, migration 017 | 지속 이상이 매 실행마다 새 finding을 누적 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| VERIFY-1 | ledger_verify 4개 하위쿼리 중 3개가 오류→0/ok:true | verify_impl.rs:293-312 | DB 미조회 상태에서 {"ok":true} 반환 | P0 | BACKEND_INFRA | NOT_VERIFIED |
| VERIFY-2 | captured_sum: net=captured−refunded vs pay=SUM(status='paid') — 부분 환불/GAME_ITEM 오탐 | verify_impl.rs:296-306 | 유일한 순액 검사가 정상 부분 환불·게임구매마다 실패 → 실환경 상시 알람 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| VERIFY-3 | audit_verify 역직렬화 실패→빈 vec→ok:true; 꼬리 절단(최신 N행 삭제) 탐지 불가 | verify_impl.rs:135-153 | 부분 fail-open + 절단 공격 무방비 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| (보존) | auto-fix 정책은 상태 표면 수정만(금전 건드리지 않음), 단일 배치 원자성 | reconciliation_impl.rs:166-222 | 안전 — 유지 | — | — | VERIFIED |
5. Ledger — migration 006, 008
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| LEDGER-4 | PAID_FEEDBACK 경로가 payment_captured 원장 미기록 | endpoints_impl.rs:859-868 | 원장이 payments 대비 captured 과소 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| LEDGER-5 | finalize_paid 가드 UPDATE의 changes 미검사 — 비정상 상태에서도 원장 기록 가능 | toss_impl.rs:299,339-374 | non-paid 주문에 capture 원장 존재 가능 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| LEDGER-6 | 단식 기록 — 계정 차원/대차 균형 없음 | migration 006 | 원장만으로 보존 증명 불가. PoC 수용 가능 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| (보존) | uidx_ledger_idem 부분 유니크 + 결정적 키; 원장은 상태 변경과 동일 배치 | migration 008:15-16, 전 작성지점 | 이중 capture 방지됨 — 테스트 검증 | — | — | VERIFIED |
6. Audit chain — checkout_impl.rs, verify_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| AUDIT-1 | 해시가 표준 HMAC-SHA256이 아닌 수제 구성(hex 중간 인코딩) | checkout_impl.rs:12-29 | 표준 도구로 독립 검증 불가 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| AUDIT-2 | 감사 키 부재 시 조용히 무키 SHA-256으로 동작 | checkout_impl.rs:14-15,49 | 키 미배포 시 체인 위조 가능 + verify가 ok 보고 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| AUDIT-3 | prev_hash 읽기와 INSERT가 다른 트랜잭션 — 포크 경쟁 | checkout_impl.rs:181-237, settle_impl.rs:82-104, purchase_impl.rs:69-123, refund_impl.rs:239-307, events_impl.rs:8-19 | 동시 작성자가 같은 prev로 분기 → 체인 포크, verify 오탐 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| AUDIT-4 | 3곳의 작성자가 저장 actor와 다른 actor를 해시 → 영구 verify 실패 | durable_impl.rs:144,148, reconciliation_impl.rs:243,274, toss_impl.rs:633-634 | 실제 변조 감지가 영구 오탐 뒤에 가려짐 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| AUDIT-5 | integrity_hash nullable — 구 행은 검증 실패로 보고 | 007_audit_retention.sql:5, verify_impl.rs:147 | 마이그레이션된 DB가 깨진 체인으로 시작 | P2 | BACKEND_INFRA | NOT_VERIFIED |
7. Outbox — checkout_impl.rs:295-335, durable_impl.rs
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| OUTBOX-2 | lease 커밋 전에 deliver_event 실행 — 겹친 dispatcher가 동일 이벤트 중복 POST | checkout_impl.rs:307-334 | 실제 중복 전달 — 소비자 멱등 필수 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| OUTBOX-3 | claim/result 배치 let _ = | checkout_impl.rs:334 | 배치 실패 무소음 — 전달된 이벤트 재전달 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| (보존) | stale-claim 회수, lease_owner 조건 전이, dead_letter, 관리자 redrive 가드 | checkout_impl.rs:299-331, durable_impl.rs:125-153 | 안전 — 유실 경로 없음(중복만) | — | — | VERIFIED |
8. 스케줄·관리자·CI
| ID | 기능 | 근거 | 위험 | 심각도 | 담당 | 상태 |
|---|---|---|---|---|---|---|
| CRON-1 | cron은 outbox dispatch + license prune만 — reconciliation 미스케줄, let _ = | lib.rs:212-222, wrangler.toml:10-11 | 지속 검증 없음 — 수동 POST 의존 | P1 | BACKEND_INFRA | NOT_VERIFIED |
| AUTHZ-1 | 관리자 게이트 3종이 kind 엄격도 불일치(개발 관리자가 recon 실행 가능, ledger/audit verify 불가) | endpoints_impl.rs:544-561,501-508, verify_impl.rs:130,288 | 운영 정책 불일치 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| TEST-1 | ledger_verify/audit_verify/settlements 호출 테스트 전무; 부분 환불 오탐 미검증 | be-cloudflare/tests/ 전체 | VERIFY-1/2가 무노출 | P1 | INDEPENDENT_QA | NOT_VERIFIED |
| TEST-2 | integration.test.mjs가 어떤 CI 잡에서도 실행 안 됨; be-cloudflare cargo test leg 없음 | .github/workflows/yetdapay.yml | 회귀 구멍 | P2 | BACKEND_INFRA | NOT_VERIFIED |
| CHECKOUT-1 | 주문 멱등 pre-check가 amount 비교 안 함(제품/플랜만) — 사후 경로는 비교 | checkout_impl.rs:103-105 vs 249-251 | 동일 키 다른 금액에 구 계약 불일치 | P2 | BACKEND_INFRA | NOT_VERIFIED |
9. 파급 결정(수정 방향)
| 우선 | 작업 | 커버하는 finding |
|---|---|---|
| P0 | w4a-refund-safety: 환불 선기록 fail-closed, 멱등키 열, 상태기계(RECORDED→REQUESTED→CONFIRMED→APPLIED + UNCERTAIN/NEEDS_REVIEW/FAILED), stuck-attempt 복구 경로, 웹훅 적용 오류 전파, provider_ref 보존, Idempotency-Key 헤더, 본문 파싱 400 | REFUND-1/2/3/6/7/8/9/10/11/12/13, INBOX-1/4 |
| P0 | w4a-settle-recovery: settlement_runs(CREATED→…→COMMITTED/FAILED/NEEDS_REVIEW) + lease/fencing + 스냅샷 + 커서 + 고아 claim 회수 + 상태 재검증 + 수수료 스냅샷 + 지급 상태 분리 | SETTLE-1/2/3/4/6/7 |
| P0 | w4a-integrity-core: financial_integrity/ 모듈 — fail-closed 쿼리 계층, 검사 결과 상태(PASSED/FAILED/INCOMPLETE/ERROR/RUNNING), 금전 불변조건(부분 환불 정확 공식), 실행 완전성 회계+커서+재개, verify_impl fail-closed, recon actor fix | RECON-1/2/3/4, VERIFY-1/2, AUDIT-4(recon), LEDGER-4/5, CHECKOUT-1 |
| P1 | w4b-exceptions-ops: exceptions/lifecycle/fingerprint + recovery operations(승인 분리, PG-호출 금지 권한) + admin API + cron 지속검증 | RECON-5, CRON-1, F3/F5 전체 |
| P1 | w4c-audit-v2: 표준 HMAC-SHA256 v2 + 원자 sequence + 키 부재 명시 오류 + v1 공존 검증 + checkpoints + 증적 자동생성 + actor/claim-fix 잔여분 | AUDIT-1/2/3/4/5, INBOX-2, OUTBOX-3 |
| P1 | w4c-admin-fe: admin Financial Integrity 화면(서버 확정 상태만 표시) | F6 |
| 필수 | w4c-fi-oracle(QA 독립 오라클) + w4d-ci-gate(financial-integrity 잡) + w4d-ops-docs(런북 문서) | TEST-1/2, F7/F8 |
10. 이미 안전한 것(유지)
- 원장 멱등 유니크 인덱스 + 결정적 키; 모든 효과 묶음이 단일
db.batch - Toss
uncertain→needs_review+ 신규 취소 전 authority 재조회 - inbox payload-sha dedup + conflict + authority 재조회(테스트 검증)
- outbox lease 조건 전이 + dead letter + 관리자 redrive 가드
- 부분 환불 누적 cap(
SUM(succeeded) ≤ amount_krw) 자체는 정확 — REFUND-1/7 경로에서만 무력화 - 관리자 게이트 존재 + DB 재검증 역할(401/403 테스트 검증)