제품 레드팀 + 신뢰성 배터리 — p4-security-reliability
오너: ⑥ INDEPENDENT_SECURITY (외부 검증 레인) 베이스라인:
e3b4d04f6b7e6ac6e727ac97e699f1386d892219(검증된 베이스: p1 durable-authority + p2 CLI + p3 콘솔) 범위: R4 §8.1 제품 플레인 공격(12 레그) + §8.2 신뢰성(7 레그), 실제 Cloudflare Worker + 로컬 D1 대상. 방법: 실제wrangler dev --localworker(Rust→wasm), 실제 로컬 D1(miniflare sqlite), 로컬 Toss 권위 스텁(127.0.0.1:8799 — worker가 도달할 수 있는 유일한 아웃바운드), 실제 spec/planner 코드 경로. 루프백만 — 외부 시스템 없음.
결과 (2026-11-04 실행, 이 워크트리)
| 배터리 | 레그 | BLOCKED | REPRODUCED | PARTIAL | NOT_EXECUTED |
|---|---|---|---|---|---|
tests/red-team/(§8.1) | 12 | 11 | 0 | 1 | 0 |
tests/reliability/(§8.2) | 7 | 7 | 0 | 0 | 0 |
판정 열거: BLOCKED = 공격/실패가 긍정 마커와 함께 설계대로 억제됨; REPRODUCED = 무인가 효과 획득; PARTIAL = 혼합; NOT_EXECUTED = 실행 불가(정직, 절대 blocked로 카운트 안 함).
§8.1 공격 레그 — be-cloudflare/tests/red-team/run.mjs
| 레그 | 공격 | 결과 | 증거 |
|---|---|---|---|
| RT-SEC-01 | SDK verify() 제거 — execute 직접 호출 | BLOCKED | anon→401, 위조 ent→401, 조작 cap→403 + capability_invalid 이벤트; 전체 PoP 경로→200 |
| RT-SEC-02 | jti/cap_id/entitlement 리플레이 + 폐기된 라이선스 사용 | BLOCKED | 증명 리플레이→401 dpop_replay; cap 재사용→409 capability_reuse; 만료 ent→401; 폐기→403 license_blocked |
| RT-SEC-03 | 크로스 테넌트 order/license/usage/sec-events/refund-attempts | BLOCKED | seller2→404 숨김 ×5, 익명→401, buyer→404; 목록 스코프/거부; 소유자→200 |
| RT-SEC-04 | 웹훅 금액 불일치, 변조된 finalize, 조작/도난 payment_key | BLOCKED | ignored:mismatch + 부작용 0; AMOUNT_MISMATCH; 조작 키→권위를 통한 404, 주문은 pending 유지, license/ledger 없음 |
| RT-SEC-05 | 중복 웹훅, 순서 바뀐 이벤트, 충돌 주입 eventId | BLOCKED | 바이트 중복→deduplicated(1 lic/1 capture); 환불은 낡은 DONE으로 부활 안 함; eventId 선점 불가(충돌 감사 + 실제 적용) |
| RT-SEC-06 | 8× 동시 소비, 4× 동시 환불, 초과 금액/음수 잔고 | BLOCKED | 정확히 1×200 + 7×409(usage=1, 카운터 Δ=1); 1 환불 차감; REFUND_AMOUNT_INVALID; 잔고 {"diamond":0} 음수 아님 |
| RT-SEC-07 | 환불 대 실행 경합, 디바이스 제한의 activate 경합 | BLOCKED | refund+exec이 원자적으로 해결(lic 폐기, 폐기 후 execute 없음); max=2에서 4 activate → 2×200+2×DEVICE_LIMIT, bindings=2 |
| RT-SEC-08 | 잘못된 iss/누락 exp/alg=none/iat ±120초 skew/jwk 없는 DPoP | BLOCKED | 모두→401; 정상→200. typ은 정보성만(PoC 패리티, 잔여물 노트) |
| RT-SEC-09 | activate→LICENSE_SIGNING_JWK 로테이션→이전 키 entitlement | BLOCKED | 재시작 후 이전 ent→401; 새 키는 정상 발급; 하드 컷오버(kid 중복 윈도우 없음 — 문서화된 잔여물) |
| RT-SEC-10 | 시크릿 탈취: bindings/licenses/events/logs/bundle | BLOCKED | bindings는 jkt만 노출; 라이선스 키 절대 목록 안 됨; 이벤트 상세는 토큰 없음; JWK.d/TOSS_SECRET/AUTH_SECRET이 로그 + wasm/shim에 부재 |
| RT-SEC-11 | 플래너 독살: 안전하지 않은 글로벌, 경로 탈출 힌트, 강제 always, 5개 적대적 형태 | PARTIAL | unsafe-global 심볼→MANUAL_REVIEW; 보호 불가→R-UNPROTECTABLE; secret→R-SECRET-CUSTODY; 형태 거부. 경로 탈출 location_hint.file은 불활성 라벨로 허용(발견 F-01) |
| RT-SEC-12 | spec 설명의 프롬프트 인젝션 문서 + 마커 무력화 | BLOCKED | 결정이 깨끗한 spec과 동일; REQUIRES_APPROVAL은 말로 제거 불가 → MANUAL_REVIEW |
§8.2 신뢰성 레그 — be-cloudflare/tests/reliability/run.mjs
| 레그 | 실패 | 결과 | 증거 |
|---|---|---|---|
| RT-REL-01 | 흐름 중 SIGKILL → 재시작 | BLOCKED | 커밋된 order/license 온전; 크래시 표시된 inbox(processing)는 재전송에서 재처리 → paid + license + processed |
| RT-REL-02 | D1 쓰기 실패(RAISE(ABORT) 트리거) | BLOCKED | capability→500, grant 행 0; execute→500, cap 미소비, usage=0; 트리거 드롭 후 복구 |
| RT-REL-03 | outbox deliver_event 실패 → dead_letter → 재구동 | BLOCKED | 라이브 cron 디스패치(/cdn-cgi/mf/scheduled) → dead_letter last_error=consumer:none; admin 재구동→pending+audit; 소비→409; seller→403 |
| RT-REL-04 | 2× wrangler dev, 동일 D1, 동시 소비 | BLOCKED | 양쪽 인스턴스에서 동일 디바이스 키페어 — 정확히 1 소비(usage=1, 카운터=1); 패배 측은 500 표면화(sqlite 경합, 잔여물) |
| RT-REL-05 | 라이선스 권위(D1 테이블) 불가 | BLOCKED | capability→500, activate→403 — 권위 실패에서 grant 없음; 복원 후 복구 |
| RT-REL-06 | 로그인 레이트 리밋 소진 + 우회 | BLOCKED | 5번째 실패→429 잠금; 잠긴 계정 + 올바른 비밀번호→429; 이메일당 스코프 |
| RT-REL-07 | 잘못된 SDK 버전 요청 형태 | BLOCKED | 누락 cap_id→400 VALIDATION; 미부여 기능→403; 잘못된 jwk→400; product-id-as-key→403; 누락 DPoP→401; 레거시 verify는 읽기 전용 유지 |
증거 아티팩트
be-cloudflare/tests/red-team/out/—red-team-results.json(HEAD sha + 실제 아티팩트 sha256에 바인딩된 레그당{id, commands, expected, actual, result, evidence}),attack-matrix.md,evidence/*.log(레그당 트랜스크립트, gitignore*.log),planner/(실제 spec+plan+evidence/record.mjs를 통한 스키마-v1 증거 레코드),rea/verification.json.be-cloudflare/tests/reliability/out/—reliability-results.json,reliability-matrix.md,evidence/*.log.- 스키마-v1 레코드:
red-team/out/planner/evidence-record.json— 진짜 spec+plan 바인딩이 존재하는 planner/spec 표면에 대해서만 발급; 제품 플레인 레그는 spec/plan이 없으므로 배터리 레그 레코드 형식(sha + 아티팩트 해시 + 실제 명령)으로 바인딩. - REA MCP:
red-team/out/rea/verification.json—build/index_bg.wasm에서inspect_wasm_artifact실행(sha24d5c2d7…; WABT가 모듈+타입 파싱, 전체 디스어셈블리는 고정 32MB 예산 초과 → 부분 증거ev_0be94b90…보유); JS 앱 분석 NOT_EXECUTED(도구가 dirs/.asar만 수용) — 정직.
신규 발견 / 관찰 (PM 트리아지용)
| ID | 심각도 | 관찰 |
|---|---|---|
| F-01 | Low | 보호 spec의 location_hint.file이 analysis.target과 교차 검사되지 않음 — 플랜 아티팩트가 공격자 선택 파일 라벨을 가질 수 있음. 파일시스템 접근 결과 없음(transform.mjs는 analysis.target/--source만 읽고 힌트는 절대 안 읽음), 결정은 분석 바운드 유지지만 다운스트림 소비자는 힌트 값을 신뢰하면 안 됨. |
| F-02 | Info | dev(샌드박스) 아이덴티티는 kind=="user" 게이트 목록 엔드포인트(예: GET /orders)에서 스코프 200이 아닌 401을 받지만 scope() 기반 읽기 API는 수용 — fail-closed 패리티 갭, 노출 없음. |
| F-03 | Info | JWS typ 헤더가 entitlement/DPoP에서 강제되지 않음(PoC 패리티 — 서명+클레임이 여전히 게이트). 드리프트된 typ 수용; 비보안 부담으로 문서화. |
| F-04 | Info | 멀티 인스턴스 공유 D1 소비 경합: 패배 인스턴스가 clean 409가 아닌 500 표면화(sqlite 경합) — dedup 불변식은 정확히 1 유지; 호출자는 재시도해야 함. |
| F-05 | Info | 레이트 리미팅은 /auth/login에만 존재(5회 실패→5분 잠금). 웹훅/라이선스 엔드포인트는 요청 수준 스로틀 없음(계약 위반이 아닌 인프라 관심사). |
| F-06 | Info | 키 로테이션은 하드 컷오버 — 진행 중 entitlement는 kid 기반 중복 윈도우 없이 서명 검사(401)에서 사망; 비행 중 클라이언트는 재활성화 필요. |
재실행
bash
cd be-cloudflare
tests/red-team/run.sh # 빌드 + .dev.vars + D1 마이그레이션/픽스처 + 양쪽 배터리
# 또는 개별적으로:
node tests/red-team/run.mjs # .dev.vars + 로컬 D1 + worker-build 출력 필요
node tests/reliability/run.mjs1
2
3
4
5
2
3
4
5
모든 테스트는 로컬 D1에 대한 루프백 전용(127.0.0.1) — staging/production에 대해 절대 실행 금지.