최종 E2E 검증 리포트 — Subagent I (독립 보안 및 QA 감사자)
과거 기록 안내
특정 날짜의 실제 스테이징 E2E 검증 결과입니다. 당시 발견한 결함과 증거를 보존합니다.
NOT_READY 판정을 최신 코드 재검증 또는 운영 환경 안전성의 증거로 해석하지 마세요.
날짜: 2026-10-30 · 대상: https://api.staging.devpay.chu.plus(Cloudflare Worker nougat-devpay-api-staging, DEV_AUTH=1, PAYMENT_MODE=staging) 방법: 독립 재검증 — 모든 테스트는 라이브 스테이징에 대한 실제 curl 요청으로 실행. 이전 클레임 신뢰 안 함. 실제 /auth/signup → /seller/onboarding 흐름으로 두 셀러 계정(seller_a495f8f8ee6f, seller_f64fbd399929) + 한 buyer 생성.
테스트 매트릭스
| # | 테스트 | 기대 | 결과 | 증거 |
|---|---|---|---|---|
| 1 | signup→login→/me + 실제 HS256 JWT | 200, eyJhbGciOiJIUzI1NiI, iss/aud/exp | PASS | 헤더 {"alg":"HS256","typ":"JWT"}; 페이로드 iss=nougat-devpay aud=nougat-api sub=usr_6a7adf2c6df1 tv=0 iat/exp 설정; sig=32B; /me→200 |
| 2 | 위조 토큰(변조된 서명) | 401 | PASS | /me + 마지막 서명 문자 플립 → 401 {"code":"UNAUTHORIZED"}; 위조 admin 클레임 JWT → 403 FORBIDDEN |
| 3 | logout → 토큰 재사용 | 401(token_version) | PASS | /me 200 → POST /auth/logout 200 → 동일 토큰 /me→401, /orders→401 |
| 4 | deactivate → 토큰 재사용 | 401/403 | PASS | POST /me/deactivate→200 → 동일 토큰→401; 재로그인→403 ACCOUNT_INACTIVE |
| 5 | 로그인 레이트 리밋(6번 잘못된 pw) | 429 | FAIL | 12번 연속 잘못된 시도 → 모두 401 invalid credentials; 올바른 비밀번호로 여전히 로그인 → 잠금 없음 |
| 6 | buyer 토큰 → /admin/users | 403 | PASS | 403 {"code":"FORBIDDEN","message":"admin only"}; seller 토큰도 403 |
| 7 | sellerA GET /orders — sellerB 누출? | 스코프됨/비어있음 | PASS | sellerA는 ord_ad12be13a96c만 봄(seller_a495f8f8ee6f); sellerB는 자기 것만 |
| 8 | sellerA POST /orders/{B}/refund | 403 | PASS | 403 {"code":"FORBIDDEN","message":"다른 판매자의 주문은 환불할 수 없습니다"} |
| 9 | sellerA PATCH /products/{B} | 403/404 | PASS | 404 {"code":"NOT_FOUND"}; 상품 변경 안 됨 |
| 10 | buyer POST /products | 403(fallback 없음) | PASS | 403 {"code":"FORBIDDEN","message":"판매자 계정이 필요합니다"} — seller_nougat fallback 없음 |
| 11 | POST /checkout simulate=success | 스테이징에서 동작 | PASS | 200 {"ok":true,"order":{"id":"ord_d479746ed2e1","status":"paid"}} |
| 12 | 유료 피드백 결제 재사용 | 409 PAYMENT_REUSED | PASS | 첫 POST /feedbacks w/ pay_892a810edfee→200 fb_969ae6856f1d; 두 번째→409 {"code":"PAYMENT_REUSED"} |
| 13 | GET /api/v1/health | 200 | PASS | 200 {"ok":true}(노트: 루트 /health = 404; /api/v1/health 사용 필요) |
보충 검사 (모두 PASS)
- sellerA
GET /orders/{sellerB_order_id}→404 NOT_FOUND(누출 없음); sellerB 자기 주문 → 200 - buyer
GET /orders/{id}→ 404; 무인증/me→ 401; 무인증/orders→ 401 - 중복
/seller/onboarding→409 ALREADY_SELLER - buyer →
/dashboard/stats403,/settlements403,POST /settlements403;/audit,/metrics→ 404(Worker에 라우팅 안 됨) - sellerA 자기 주문 자체 환불 → 200(sanity: 스코핑 방향 올바름)
- 코드 검증: 원자적 정산 클레임
UPDATE … AND settlement_id IS NULL(settle_impl.rs:61); 모든json_setunit 경로의safe_json_key화이트리스트(refund/events/purchase);sub_event셀러 소유권 검사(events_impl.rs:34-44); PBKDF2-50k 해싱;ct_eq상수 시간 비교;verify_token_dbDB 측 role/seller/status/token_version 재검사.
신규 / 여전히 깨진 발견 (라이브 검증)
| 심각도 | 발견 | 증거 |
|---|---|---|
| P0 | Buyer JWT가 모든 주문을 환불 가능. refund()가 seller_scope_db 호출 → buyer의 seller_id가 비어 있음 → None → map_or(true, …)가 "스코프 없음"을 admin으로 취급 → 환불 진행. | 라이브: buyer 토큰 POST /orders/ord_55565f0b1710/refund → 200 {"ok":true,"status":"refunded"}. prod 경로에도 동일 홀 존재(JWT auth buyer도 seller 스코프 없음). 수정: 공유 시크릿/admin만 scope=None으로 통과 가능; 환불은 seller/admin role 필요. |
| P0 | DEV_AUTH=1 백도어가 스테이징에 라이브. X-Dev-User: <anything>(토큰 없음) → 무인증 접근; seller_scope_db가 이를 seller_nougat으로 매핑. | 라이브: GET /orders -H "X-Dev-User: attacker" → 200, PII 포함 모든 6개 주문; POST /products → seller_nougat로 prod_06d991397b50 생성.(제자리 유지 — DELETE는 dev-user가 없는 seller role 필요.) 스테이징이 실제 데이터와 격리된 경우에만 허용; prod에서는 DEV_AUTH=0/부재여야 함. |
| P1 | 로그인 레이트 리미터가 스테이징에서 사망. 12번 잘못된 비밀번호 시도 → 각각 401, 절대 잠금 안 됨, 올바른 비밀번호 여전히 성공. | 스테이징 D1에 login_attempts 테이블(migration 011) 누락으로 추정 — record_login_fail이 DB 오류를 삼키고(let _ =) fails=0을 반환하여 fails>=5가 절대 발동 안 함. 수정: 마이그레이션 적용 + 실패 카운터 fail-closed. |
| P1 | 크로스 테넌트 읽기 누출: 인증된 모든 buyer가 모든 주문을 읽음. GET /orders는 role=="seller"일 때만 스코프; buyer는 _sid=None → 무스코프 SELECT … FROM orders. | 라이브: buyer 토큰 GET /orders → 200, seller_a495f8f8ee6f, seller_f64fbd399929, seller_nougat의 모든 6개 주문(buyer 이메일, 금액, 결제 id). GET /feedbacks(모든 메시지)와 GET /licenses(모든 라이선스 메타데이터; key 컬럼은 비노출)도 동일. 주문당 GET /orders/{id}는 올바르게 스코프(404) — 목록/레코드 불일치. |
| P1 | POST /fulfillments/{id}/retry에 소유권 검사 없음(코드 리뷰, events_impl.rs:195-198) — auth()만; 모든 유효 토큰이 실패한 이행을 통화 지급으로 강제 가능. 라이브 악용 안 함(대상 실패 이행 없음). | 환불 홀과 같은 클래스. |
판정: 공개 베타 NOT_READY
13개 처방 테스트 중 12개 통과 — JWT 발급/검증, 폐기, 비활성화, admin 게이팅, 셀러 스코프 주문 목록, 크로스 셀러 쓰기 차단, 체크아웃 simulate, 결제 재사용이 모두 진짜로 수정되고 종단간 검증됨. 그러나 세 개의 라이브 검증된 홀이 출시를 차단:
- Buyer→refund-any-order(P0) — 스테이징 설정이 아닌 프로덕션 코드 경로에 존재하는 사소하게 악용 가능한 파괴적 쓰기.
- 무스코프 목록 엔드포인트(P1) — 등록된 모든 buyer가 모든 셀러의 주문/PII를 덤프 가능.
- 죽은 로그인 레이트 리미터(P1) — 무제한 비밀번호 추측.
- DEV_AUTH=1 스테이징 백도어(env 위생 P0) — 격리된 dev 스테이징에는 괜찮지만, 스테이징이 실제 데이터를 보유하거나 플래그가 prod로 누출되면 치명적.
권장 게이트: 환불 스코프 수정(map_or(true) → seller/admin role 요구), role로 모든 목록 엔드포인트 스코프(buyer→자기 buyer_ref만), login_attempts 수리(마이그레이션 + fail-closed), prod 배포 검사에서 DEV_AUTH=0 어서션. 그 네 가지 랜딩 후 재감사 → 아마 WITH_LIMITATIONS(mock 결제, mock IdP).