최종 보안 감사 — Subagent I
과거 기록 안내
당시 확인한 보안 결함과 일부 후속 재검증을 기록한 과거 독립 감사입니다.
근거와 날짜를 보존하고 해당 문제가 현재도 존재하는지는 최신 보안 테스트로 판단하세요.
P0
- 스테이징 완전 무인증: DEV_AUTH=1 + X-Dev-User로 모든 방문자가 셀러 접근(lib.rs:99, auth_impl.rs:198)
- 크로스 셀러 IDOR: /projects,/products,/feedbacks,/licenses 무스코프; refund/sub_event/ff_retry 소유권 검사 없음; buyer JWT→seller_nougat fallback 에스컬레이션
- JWT 비표준: audit_hash 서명 ≠ HS256; Axum 검증 불가; 클레임 불일치(seller_id vs sid)
P1
- 세션 폐기 없음(24시간 토큰이 deactivate/logout 후에도 생존)
- Prod auth 붕괴(스코프 라우트에서 JWT 사망; 공유 시크릿=전능)
- 유료 피드백 리플레이(payment_id 절대 기록 안 함 → UNIQUE 사장)
- format!() unit이 json path로 들어가는 SQL 인젝션(refund/events/purchase)
- 이중 정산 경합(AND settlement_id IS NULL 없음)
- 레이트 리미팅 없음(login/license-verify/feedbacks)
- 스테이징의 데모 자격 증명(demo1234 admin)
- 마이그레이션 드리프트(Axum은 008에서 정지; 001 파일 두 개)
P2
- 비상수 시간 비교, 로그아웃 엔드포인트 없음, 활성화 엔드포인트 없음, 계정 열거 가드 없음
검증됨 — 독립 재감사, 2026-10-30(api.staging.devpay.chu.plus 대상 라이브 curl)
| 발견 | 검증됨 |
|---|---|
| 스테이징 완전 무인증(DEV_AUTH=1, X-Dev-User) | CONFIRMED — 여전히 라이브. GET /orders -H "X-Dev-User: attacker"(토큰 없음) → 200, PII 포함 모든 주문; POST /products → seller_nougat로 prod_06d991397b50 생성. 의도된 스테이징 플래그(wrangler.toml [env.staging.vars] DEV_AUTH="1") — 절대 prod에 도달하면 안 됨; 스테이징은 실제 데이터를 보유하면 안 됨. |
| 크로스 셀러 IDOR | 부분 수정. FIXED+검증: 셀러 주문 목록 스코프(A는 자기 것만); 크로스 셀러 GET /orders/{id}→404, refund→403, PATCH /products/{id}→404; buyer POST /products→403(seller_nougat fallback 없음); sub_event 소유자 검사(events_impl.rs:34-44). 여전히 깨짐: (a) buyer JWT POST /orders/{any}/refund→200 — seller_scope_db=None→map_or(true)가 비셀러를 admin으로 취급(라이브 검증: buyer가 ord_55565f0b1710 환불); (b) GET /orders,/feedbacks,/licenses,/products가 비셀러 역할에 무스코프 — buyer 토큰이 모든 테넌트의 주문/PII를 덤프(라이브 검증); (c) ff_retry는 여전히 auth 전용, 소유권 검사 없음. |
| JWT 비표준 | 수정됨. 실제 HS256 발급(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 헤더, 32바이트 서명), 클레임 iss=nougat-devpay aud=nougat-api sub role seller_id tv iat exp; 변조 서명→401; 위조 admin 클레임→403. |
| 세션 폐기 없음 | 수정됨. token_version이 D1로 검사(verify_token_db); logout→토큰 재사용→401; deactivate→401 토큰 + 403 로그인(ACCOUNT_INACTIVE). 모두 라이브 검증. |
| Prod auth 붕괴 | 완화됨(코드 리뷰). verify_token_db가 D1에서 role/seller_id/status/token_version을 재독 — 클레임이 더 이상 신뢰되지 않음. Buyer 토큰은 scope=None이 허용 전체로 취급되는 스코프 라우트에 여전히 도달(환불 홀 참조) — 잔여물, 완전히 붕괴하지 않음. |
| 유료 피드백 리플레이 | 수정됨. POST /feedbacks w/ pay_892a810edfee→200; 재사용→409 PAYMENT_REUSED(used_at + feedbacks.payment_id + UNIQUE 백스톱). 라이브 검증. |
| json path unit의 SQL 인젝션 | 수정됨(코드 리뷰). safe_json_key [a-zA-Z0-9_]{1,64} 화이트리스트가 모든 json_set('$.{unit}') format!() 사이트를 가드 — refund_impl.rs:57, events_impl.rs:205, purchase_impl.rs:53. |
| 이중 정산 경합 | 수정됨(코드 리뷰). 원자적 클레임 UPDATE orders SET settlement_id=?1 WHERE id=?2 AND settlement_id IS NULL(settle_impl.rs:61). |
| 레이트 리미팅 없음 | 여전히 깨짐(라이브). 로그인: 12번 잘못된 시도→각각 401, 429 없음, 계정 잠금 없음 — record_login_fail이 DB 오류를 삼킴(let _ =)으로 fails=0; 스테이징 D1에 login_attempts(migration 011) 누락 또는 낡은 worker 배포로 추정. license-verify/feedbacks 리미팅은 재테스트 안 함. |
| 스테이징의 데모 자격 증명 | 재테스트 안 함(demo1234 로그인 시도 없음; 현재 수정과 무관). |
| 마이그레이션 드리프트 | 위와 일치 — 누락된 마이그레이션이 죽은 레이트 리미터의 추정 원인. |
| 비상수 시간 비교 / 로그아웃 없음 / 비활성화 없음 | 수정됨. 서명+비밀번호 비교에 ct_eq 사용; POST /auth/logout과 POST /me/deactivate 라우팅 및 라이브 검증. 계정 열거 가드 존재(알 수 없는 이메일도 실패로 카운트 — 리미터가 죽은 동안은 무의미). |
평가: 핵심 auth P0(JWT, 폐기, 온보딩, admin 게이팅, 크로스 셀러 쓰기 IDOR, 유료 피드백 리플레이)가 종단간 검증됨. NOT_READY — 남은 라이브 검증 블로커: buyer-refund-any-order(P0), buyer용 무스코프 order/feedback/license 목록(P1), 죽은 로그인 리미터(P1), DEV_AUTH 백도어(P0 env 위생).