세 발견 모두 라이브 DB에서 정책·권한·트리거를 직접 조회해 사실로 확인했다. 앱 UI가 아니라 Supabase API(PostgREST)를 직접 호출하면 발동한다 — 개발자 도구를 아는 회원이면 가능.
| # | 취약점 | 영향 | 조치 |
|---|---|---|---|
| C1 | 회원이 자기 profiles.coins / membership_type / is_approved를 직접 UPDATE 가능 (RLS가 '본인 행'이면 전 컬럼 허용) | 무한 버디 충전·무료 VIP·승인 게이트 우회 → 버디 경제 전체 무력화 | 0138 마이그레이션 준비 완료 — 적용 승인 대기 backend/migrations/0138_security_sensitive_columns.sql (민감컬럼 가드 트리거: 정상 RPC·관리자 웹은 그대로 동작) |
| C2 | 회원이 자기 users.role='admin' 직접 UPDATE 가능 | 관리자 권한 상승 → 전 회원 연락처·실명·채팅 열람, 강퇴 회원 status 자가 복구도 가능 | |
| C3 | 초청 신청 INSERT 시 status='confirmed' 자가 설정 가능(검증 없음) | 작성자 5버디 과금 없이 '확정' 표시 + 자동환불 차단 | |
| C4 | meetup_signups에 UPDATE 정책 자체가 없어 관리자 웹의 초청 신청 승인이 조용히 0행 갱신(모바일 확정은 RPC라 정상) | 관리자 웹 승인이 저장 안 됨(무음 실패) | 0138에 admin UPDATE 정책 포함 |
| 심각도 | 내용 |
|---|---|
이중 차감 경쟁조건 — unlock_profile(0112)·view_received_like(0067)·unlock_profile_view(0124)·reveal_visitor(0068): 중복 검사(exists)가 잠금(FOR UPDATE) 밖이라 동시 더블탭 시 2배 차감/1회 제공 가능. 발생 확률 낮음(2026-06-16 QA에서도 지적된 패턴). | |
상담 메시지 위조 — support_messages UPDATE 정책이 sender 제한 없음: 회원이 자기 스레드 안에서 기존 메시지를 '운영자 답변'처럼 고칠 수 있음(본인 화면 한정, 실피해 낮음). |
| 심각도 | 위치 | 내용 |
|---|---|---|
| 높음 | api/reviews.ts:133 | 사진 첨부 후기 등록에서 사진 선택을 취소해도 후기가 그대로 등록됨 |
| 높음 | profile-edit.tsx:163 ↔ api/account.ts:143 | 같은 캐시키 ['my-contact']에 다른 형태 데이터 → 연락처 화면 다녀온 직후 프로필 수정 열면 실명이 빈칸(null 덮어쓰기 경로 존재) |
| 높음 | explore.tsx:107 · member/[id].tsx:230 | 찜 낙관적 업데이트 실패 시 롤백 없음 — 실제 찜 안 됐는데 '찜 완료'로 표시 |
| 중~높 | (tabs)/interests.tsx:87 | 찜 읽음 처리가 최초 마운트 1회뿐 → 이후 찜은 탭을 열어도 배지가 안 지워짐 |
| 중간 | api/unlocks.ts:272 | 매칭 요청 성공 후 ['matches'] 캐시 무효화 누락 → 새 채팅방이 바로 안 보임 |
| 중간 | api/photos.ts:203·270 | 사진 개수 조회 에러 무시(5장 제한 우회+대표사진 덮어씀 가능), 대표사진 3단계 update 중간 실패 시 대표 없음 상태 |
| 중간 | api/meetups.ts 외 6곳 | 에러를 빈 목록/null로 삼키는 패턴 — 일시 오류가 "없음"으로 위장(재시도 UI 없음) |
| 중간 | api/account.ts:147 | 연락처 조회 실패가 '미등록'처럼 표시 |
| 낮음 | api/matches.ts:183 | 안읽음 집계 1000행 캡(도달 확률 낮음) |
| 심각도 | 위치 | 내용 | 상태 |
|---|---|---|---|
| 높음 | admin api/band-image/route.ts:5 | 도메인 검사 정규식에 경계 없음 → evilnaver.net 등으로 오픈 프록시(SSRF 경유지) 가능(공개 라우트) | 수정 대기 |
| 높음 | admin CoinEditor.tsx:19 | 버디 증감이 읽고-계산-쓰기(비원자) → 회원이 동시에 소비하면 버디 유실/복원. DB 원자 증감으로 교체 필요 | 수정 대기 |
| 중간 | admin BandProfilesList 등 4곳 | useState(prop) stale — 60초 자동갱신이 목록에 반영 안 됨(과거 동일 패턴 버그 이력) | 수정 대기 |
| 중간 | admin members/page.tsx:52 | 전체보기/검색/받은관심 1000행 캡 — 회원 1000명 넘으면 조용히 누락(엑셀 포함) | 수정 대기 |
| 중간 | admin MeetupApplicants·NoticeManager | 실패해도 승인 진행/에러 무시(무음 실패) | 수정 대기 |
| 중간 | backend/apply-migration.mjs | 트랜잭션 없이 적용 — 중간 실패 시 반적용 상태 | 권고 |
| 훅 4건(guard/secret-guard) | -Id 문자열 우회 · MultiEdit 미검사 · JSON 파싱실패 fail-open · base64 패딩 시크릿 미추출 | ✅ 즉시 수정+테스트 통과 | |
결론: junk 파일 때문이 아니라 ① 코드 축소(R8) 미적용 ② 사진급 PNG 이미지가 원인. 압축 전 기준 구성:
| 구성 | 크기(압축 전) | 비중 | 내용/판정 |
|---|---|---|---|
| classes.dex ×6 | 53.9 MB | ≈61% | 자바/코틀린 코드(RN·Expo·Firebase). R8 minify/shrinkResources가 꺼져 있음(gradle 기본 false) — 켜면 통상 40~60% 감소 |
| lib/arm64-v8a ×25 | 20.1 MB | ≈23% | 네이티브 라이브러리(hermes·reactnative 등). 필수 — 이미 arm64 단일 ABI로 최적화됨 ✅ |
| res (이미지·폰트) | 10.0 MB | ≈11% | PNG 7.6MB 중 남성 기본 프사 3장(man1~3.png)=6.0MB+인앱 스플래시 PNG 1.6MB — 사진인데 PNG라 5~8배 비대. WebP/JPG 전환 시 ~6MB 절감. 폰트 1개 0.9MB(프레임워크 번들 기본 폰트 — src에 명시적 사용처 없음, R8/폰트 트리셰이킹 대상) |
| assets (JS 번들) | 5.1 MB | ≈6% | Hermes JS 번들 — 정상 범위 |
| 기타(resources.arsc·bouncycastle 등) | ~3.4 MB | ≈4% | 대부분 의존성 부속 — R8이 함께 정리 |
| 조치 | 예상 절감 | 리스크 |
|---|---|---|
① android.enableMinifyInReleaseBuilds=true + shrinkResources (R8) | 15~25MB | 중 — RN 계열은 keep 규칙 필요할 수 있어 빌드 후 전체 기능 스모크 테스트 필수 |
| ② man1~3.png → WebP/JPG(사진이라 투명도 불필요) | ~5.5MB | 낮음 |
| ③ splash-loading.png(1.6MB) → JPG | ~1.3MB | 낮음 |
| ④ 미사용 vector-icons 폰트 제거 + resConfigs "ko" 지정 | ~1MB | 낮음 |
※ APK 재빌드가 필요하므로(OTA 불가) 다음 APK 배포 때 묶어서 적용 권장. 로컬 정리 후보(앱 용량 무관): mobile/assets 아이콘 백업 2개(0.6MB), dist 13MB(재생성물), android build 산출물 — 회원 데이터 아님·삭제 전 확인 원칙 적용.
생성: Claude(Fable 5) 자율 개발팀 · 리뷰 방법: 영역별 병렬 코드 리뷰 + 상위 발견 실DB 검증(pg_policies·column_privileges·triggers 직접 조회) + APK unzip 분해