bibimbap/docs/changes/2026-06-29-w3-w4-features.md

16 KiB

kind title session_id created_at status related_design_summary related_design related_impl related_verification related_report related_security_checklist related_prev_change
change W3(잔여) + W4 기능 구현 (W3-1·W3-4·W3-3·W3-5·W4) 20260629-100849 2026-06-29 implemented ../work-log/2026-06-23-w2-w4-full-design-summary.md ../../.atp/work-session/20260623-104307/implementation/ ../../.atp/work-session/20260629-100849/implementation/ ../../.atp/work-session/20260629-100849/verification.md ../../.atp/work-session/20260629-100849/report.md ../security/security-remediation-checklist.md ./2026-06-24-w2-jam-platform.md

W3(잔여) + W4 기능 구현 변경 이력 (W3-1·W3-4·W3-3·W3-5·W4)

상위 설계 인덱스: W2+W3(잔여)+W4 풀설계 통합 요약 §설계 인덱스 W3-1/W3-3/W3-4/W3-5/W4. 설계 본문: .atp/work-session/20260623-104307/implementation/W3-*.md · W4-badges-design.md · 파일 소유권/구현 보고: .atp/work-session/20260629-100849/implementation/ownership*.md · 검증: .atp/work-session/20260629-100849/verification.md · 세션 보고: .atp/work-session/20260629-100849/report.md.

개요

W2 게임잼 워크스트림(2026-06-24-w2-jam-platform.md) 완료 후, W2-W4 풀설계 11기능 중 미구현으로 남았던 잔여 5기능을 순차 구현했다. W3-2(댓글/리뷰)는 이미 완료 상태였고, 본 작업은 태그+검색(W3-1) → 메인허브(W3-4) → 포스팅보드(W3-3) → 업로드보안(W3-5) → 배지/평판(W4) 순서로 각 기능을 impl → verify → commit 롤백 경계로 묶어 진행했다(5 feat 커밋).

서브 커밋 핵심 L1 누적
W3-1 태그 + 검색 확장 35f1dc3 tags/game_tags/jam_tags/game_views + searchGamesAdvanced(다중태그 AND/OR·정렬키6·방문수·리뷰정렬·잼라우트) + TagController(sanitize/금칙어/CONTENT_MODERATE) 235/235
W3-4 메인 허브 e28fa60 keyset 페이징(혼합방향 OR분해) + 진행중 잼 배너(/games/search?jam=) 243/243
W3-3 포스팅 보드 6047a39 posts/categories + POST_WRITE enforcement + commonmark/jsoup sanitize + SsrfSafeFetcher(9항목) + 유니티 피드 폴링(@Scheduled) 287/287
W3-5 업로드 보안 08d191f ZipSecurity(zip-slip/심링크/zipbomb/포맷/권한게이트) + ATOMIC_MOVE swap + game_upload_audit_log 318/318
W4 유저 배지/평판 305cc73 badges/user_badges/reputation_events + 자동(임계)·수동(BADGE_MANAGE) 하이브리드 + reputation 감사 + 리뷰작성자 표시 347/347

커밋 범위: 35f1dc3(W3-1) → e28fa60(W3-4) → 6047a39(W3-3) → 08d191f(W3-5) → 305cc73(W4). 모두 feat/v2 브랜치.

신규 DDL 5종: docs/tag-ddl.sql · docs/games-hub-ddl.sql · docs/board-ddl.sql · docs/game-upload-ddl.sql · docs/badge-ddl.sql(전부 db/schema.sql 동기). 신규 pom 의존 2종: jsoup 1.17.2 + commonmark 0.22.0(W3-3).


W3-1 — 태그 + 검색 확장 (35f1dc3)

DDL (신규 4테이블 + games 컬럼 1)

객체 역할 핵심 제약
tags 통합 태그 사전 정규화 slug 활성 UNIQUE
game_tags 게임-태그 조인 (game_id, tag_id) UNIQUE, #{} 바인딩
jam_tags 잼-태그 조인 (jam_id, tag_id) UNIQUE
game_views 방문수 dedupe 원장 최근 interval 내 재방문 제외 근거
games.view_count 누적 방문수 캐시 컬럼 game_views 적재 시 증가
  • DDL 권위: docs/tag-ddl.sql(멱등) + db/schema.sql 동기.

런타임 동작

  • GamesMapper.searchGamesAdvanced — 다중태그 AND/OR 분리(GameTagsMapper.listGameIdsByTagsAll/...Any, AND ⊆ OR 불변), 정렬키 6종, 방문수·리뷰(game_review_stats 소비) 정렬, 잼 필터 라우트. SearchCriteria POJO 로 파라미터 캡슐화.
  • SearchControllerGET /games/search 태그·정렬·잼 파라미터 처리(파라미터 전수 #{}). index.jsp SSR.
  • GameViewsMapper.existsRecentView — 최근 방문 dedupe(설계 dead param sinceTs 제거). GameController 방문 적재 훅.
  • TagController — 태그 생성/부착. TagSanitizer(normalize + banned-words.txt 금칙어) + CONTENT_MODERATE 게이트(신규 권한키 미도입, W1 선언키 재사용) + CSRF.

의미보존 편차(verification 인지)

  • insertTagint + generatedKeys 패턴(설계 표현 대비 의미보존).
  • TagControllerGamesMapper 주입으로 부착 대상 게임 소유자 확인(설계 의도 충족).

검증

L1 235/235 GREEN(baseline 190→235, +45 신규테스트, 회귀 0). L2 격리 throwaway DB: alias 케이스폴딩 인용/비인용 대조, NULLS LAST, AND ⊆ OR, ILIKE, interval dedupe. AC-1/2/4/5 + 보안 VP PASS.


W3-4 — 메인 허브 (e28fa60)

DDL

신규 테이블 0(GamesMapper/JamsMapper 확장만). docs/games-hub-ddl.sql 는 keyset 인덱스 등 허브 지원 객체.

런타임 동작

  • GamesMapper keyset 페이징 — 혼합 정렬방향을 OR 분해한 row-comparison 으로 경계 무중복·무누락 보장(검색·비검색 단일화). 단계1(keyset 더보기) + 단계2(진행중 잼 배너) 통합 구현.
  • JamsMapper.listActive(limit=1) — 진행중 잼 1건 배너용. WebMvcController 가 hub 에 주입.
  • 진행중 잼 배너 → W3-1 GET /games/search?jam={slug} 라우팅(cross-W 정합).

검증

L1 243/243 GREEN(회귀 0, 편차 0). L2: keyset 혼합방향 OR분해 경계 무중복·무누락·tie-break 실측(throwaway DB). AC-T2/T3/T4 + 단계무결성 + 보안 VP PASS. BibimbapApplicationTests 무변경(GamesMapper/JamsMapper @MockBean 기존 등록 재사용).


W3-3 — 포스팅 보드 (6047a39)

가장 복잡한 워크스트림(SSRF 9항목·md sanitize·다매퍼·@Scheduled·pom 의존). 검증 단계에서 verification + adversarial SSRF 감사 2중 검증이 vacuous(거짓통과) SSRF 결함을 포착해 backward 1회 보정(아래 §SSRF 강화)했다.

DDL (신규 4테이블) + pom 의존

객체 역할
post_categories 게시판 카테고리
posts 게시글(마크다운 본문)
unity_feed_sources 유니티 외부 피드 소스
unity_feed_items 폴링 적재 피드 아이템(dedupe)
  • DDL 권위: docs/board-ddl.sql(멱등) + db/schema.sql(append) 동기.
  • pom 신규 의존: jsoup 1.17.2(1.14+ Safelist API 정합) + commonmark 0.22.0. 오프라인 빌드용으로 프록시 CA 주입 1회 pre-resolve 후 ~/.m2 캐시(이후 오프라인 빌드 가능).

런타임 동작

  • PostController — 게시글 작성/조회. POST_WRITE enforcement 연결(W1 선언키의 첫 소비처) + CSRF.
  • PostMarkdownService — commonmark 렌더 → jsoup Safelist sanitize(XSS 차단). 검증에서 XSS sanitize SOUND.
  • OgPreviewService / UnityFeedPollerSsrfSafeFetcher 단일 경유 외부 fetch. FeedParser(RSS 2.0 / RFC1123 pubDate).
  • UnityFeedPoller @Scheduled 폴링 + dedupe insert(SchedulingConfig @EnableScheduling 신규 활성).
  • PostAdminController / UnityFeedAdminController — 카테고리·피드소스 관리(JSP 는 프로젝트 컨벤션 scriptlet + HtmlUtils.htmlEscape 로 정렬, JSTL 의존 부재 대응).

SSRF 9항목 (SsrfSafeFetcher)

scheme allowlist · 사설/루프백 IP 차단 · DNS rebinding 방어 · redirect 재검증 · size/timeout 상한 등 9항목. 초기 구현은 Host restricted-header 를 통한 fetch 가 전건 empty 가 되어 테스트가 vacuous(거짓통과) 였고 CGNAT(100.64/10) 등 차단 범위갭이 있었다. backward 보정에서:

  • option-b hostname-connect 전환 — 사전 resolve 한 IP 를 차단 검사 후 hostname 으로 직접 connect(restricted-header 우회 제거).
  • 차단 대역 확장(CGNAT 등).
  • FeedParser RFC1123 pubDate 파싱 robust 화(선행 요일토큰 요일-날짜 불일치에도 견딤 — orchestrator 마이크로수정).

검증

backward: verification FAIL(L1 283, 2fail+1err) + adversarial SSRF 감사 CRITICAL → impl fix1 → 재검증 FAIL(FeedParser 잔존) → orchestrator 마이크로수정 → L1 287/287 GREEN. L2: board keyset row-comparison · insertIgnoreDup ON CONFLICT · FK(throwaway DB). adversarial 감사: XSS SOUND, 보정 후 SSRF 우회 차단.


W3-5 — Unity WebGL 업로드 보안 보강 (08d191f)

DDL (신규 1테이블)

  • game_upload_audit_log — 업로드 감사(outcome CHECK, FK, reject_reason enum). docs/game-upload-ddl.sql(멱등) + db/schema.sql(append) 동기.

런타임 동작

  • ZipSecurity(+ ZipRejectException) — zip-slip canonical 경계 + 심링크 거부 + zip bomb(getCompressedSize 0 회피·3중상한) + 포맷검증(wasm/data/loader) + 권한게이트. 거부 사유 enum 11종.
  • GameUploadController — 게이트 순서 정렬 + 임시추출 후 ATOMIC_MOVE swap(원자 교체) + audit 적재. dead code 제거.
  • GameUploadAuditMapper(+ GameUploadAudit/GameAssetOwner POJO) — #{} only, 컬럼 8 정합.
  • GameAssetCleanupService — 자산 생명주기 정리(UUID 검증 + 경계, gameRoot 미터치).

검증

L1 318/318 GREEN(편차 0). L2: audit insert · outcome CHECK · FK · findGameByWebglUuid alias(throwaway DB). adversarial zip-slip 감사 SOUND — 심링크 TOCTOU 구조불가, 경로탈출/유니코드/zipbomb/LIKE주입 전수 차단. LOW 2건(split 빈세그먼트·static 트리 .tmp 노출)은 open_item.


W4 — 유저 배지 / 평판 (305cc73)

DDL (신규 3테이블) + 권한키

객체 역할 핵심 제약
badges 배지 카탈로그 badges_type_check
user_badges 유저 보유 배지 ux_user_badges_active 부분유니크(revoked 행 제외 → 회수후 재획득)
reputation_events 평판 이벤트 감사 reputation_events_type_check IN(REVIEW_WRITTEN, GAME_UPLOADED), source_ref 부분유니크 dedupe
  • DDL 권위: docs/badge-ddl.sql(멱등) + db/schema.sql(line 1075/1100/1127) 동기.
  • PermissionKeys.BADGE_MANAGE 신규(4번째 권한키) — W1 권한 카탈로그 카운트 3→4 갱신. PermissionCatalogVerifiervalues() 순회(하드코딩 리스트 아님)라 키 추가에 자동 적응, 카운트 가정 테스트 회귀 0.

런타임 동작

  • BadgeService / ReputationService — 자동(임계 도달 시 부여)·수동(grant/revoke) 하이브리드. BadgeCatalogSeeder 시드.
  • 자동 부여 훅 2곳: GameReviewController.listReviews 경로(리뷰 작성) + GameController 게임 업로드 경로 → reputationService.record. event_type CHECK 2종과 정합.
  • BadgeManageController/manage/badges grant/revoke. 진입부 순서 CSRF(403) → 미인증(401) → BADGE_MANAGE 게이트(403).
  • 표시: 리뷰 작성자 배지 칩(listReviews JSON + game-detail.jsp JS, UserBadgesQueryMapper.listActiveBadgeKeysByUserIds 배치쿼리로 N+1 차단) + profile.jsp 본인 배지.

검증

L1 347/347 GREEN(baseline 318→347, BadgeServiceTest 12 + BadgeManageControllerTest 14 + GameReviewControllerTest 회귀 3). W1 권한 카탈로그 회귀 0(런타임 로그 "권한 카탈로그 시드 완료: 4 키", 카운트 가정 테스트 전부 GREEN). L2: 자동부여 멱등 · 회수후 재획득 부분유니크 · reputation dedupe · countActive 집계 · 배치 alias camelCase 인용 · CHECK/FK(throwaway DB). contextLoads 신규 @MockBean 6.


보안 (프로젝트 보안 원칙 추적)

본 작업에서 신설·연결된 인가/검증/방어 게이트. 모두 단위 + L2 DB 계약(+ 보안핵심 2건은 adversarial 코드추적 감사)으로 검증.

게이트 적용 W 내용
CONTENT_MODERATE 재사용 W3-1 태그 생성/부착 게이트. W1 선언키 재사용(신규키 0).
POST_WRITE enforcement W3-3 W1 선언키의 첫 enforcement 연결(이전 소비처 0). 게시글 작성 게이트.
BADGE_MANAGE 신규 W4 4번째 권한키 신규. 배지 grant/revoke 게이트(/manage/badges). W1 카탈로그 3→4.
SsrfSafeFetcher 9항목 W3-3 scheme allowlist·사설/루프백 IP 차단·DNS rebinding·redirect 재검증·size/timeout. option-b hostname-connect(restricted-header 우회 제거).
마크다운 sanitize W3-3 commonmark 렌더 후 jsoup Safelist sanitize(XSS 차단). adversarial XSS SOUND.
zip-slip / 심링크 / zipbomb W3-5 ZipSecurity canonical 경계 + 심링크 거부 + 3중상한. adversarial zip-slip SOUND. ATOMIC_MOVE 원자 교체.
방문수 dedupe W3-1 game_views interval 기반 재방문 제외(view_count 부풀림 방지).
CSRF 전수 전 W(상태변경) 모든 상태 변경 핸들러 CSRF 선검증 → 누락 시 403.
MyBatis ${} 0건 전 W 신규 매퍼 전수 #{} 바인딩, 동적 치환 0.
§33 alias 케이스폴딩 W3-1/4, W4 VIEW/집계/배치 매퍼 camelCase alias 큰따옴표 인용 — Postgres 소문자 폴딩 회피.

검증 결과 요약

최종 컨테이너 ./mvnw -o test(eclipse-temurin:21-jdk + ~/.m2 마운트, 오프라인) 347/347 GREEN(baseline 190 → 누적 +157 신규테스트, 회귀 0).

서브 L1 누적 L2 dev DB contract(격리 throwaway) 비고
W3-1 235/235 GREEN PASS(alias 폴딩 대조·NULLS LAST·AND⊆OR·ILIKE·interval dedupe) +45 신규, 편차 2(의미보존)
W3-4 243/243 GREEN PASS(keyset 혼합방향 OR분해 무중복·무누락·tie-break) 편차 0
W3-3 287/287 GREEN PASS(board keyset·insertIgnoreDup·FK) backward 1회(verification+adversarial SSRF 2중검증이 vacuous 포착)
W3-5 318/318 GREEN PASS(audit·outcome CHECK·FK·alias) adversarial zip-slip SOUND
W4 347/347 GREEN PASS(멱등·재획득 부분유니크·dedupe·집계·배치 alias·CHECK/FK) W1 카탈로그 3→4 회귀 0
  • L2 비파괴 원칙: 모든 L2 contract 는 격리 throwaway postgres:16(생성 → DDL 적용 → 실측 → DROP)로 수행. 사용자 dev(bibimbap) DB 무접촉(전 W).
  • 2중 검증 가치: W3-3 에서 verification(테스트 실행)이 통과시킨 vacuous SSRF 를 adversarial 감사(코드추적)가 포착 → option-b 전환으로 실질 차단 확보.

상세는 .atp/work-session/20260629-100849/verification.md + report.md §verified_by_me 참조.


미적용 / 이월 (needs_user_verification)

항목 사유
실 dev DB(bibimbap) DDL 미적용 L2 는 throwaway DB 로 통과했으나 실 dev DB 에는 미적용(파괴적 조작 회피). 신규 DDL 5종(tag/games-hub/board/game-upload/badge) + (W2 carryover) jam DDL 3종 = 총 8종db/apply-local-ddl.sh 로 일괄 적용 필요(§6 수동게이트). L3 선행.
L3 런타임 스모크(WAR 기동 후) ① 검색 /games/search 태그·정렬·잼필터 + 방문수 dedupe ② 허브 / keyset 더보기 + 진행중 잼 배너 ③ 포스팅 작성(POST_WRITE)/sanitize 렌더/OG 미리보기/유니티 피드 폴링 + ★실 HTTPS 외부 fetch 동작(option-b hostname-connect 실 TLS — 단위는 HTTP만) ④ 업로드 zip-slip/포맷 거부 + 정상 Unity 빌드 + replaceUuid 교체 ⑤ 배지 자동부여(리뷰·업로드)·수동 grant/revoke(BADGE_MANAGE)·리뷰작성자 칩. 자동 미수행.
W4 표시 deferral 리뷰작성자 배지 경로만 구현. 프로필 myBadges 주입·게임카드 creator 배지 미구현(의도된 deferral). 게임카드 SSR 은 WebMvcController hub + SearchController 가 담당(W3-4/타 W 산출물)이라 본 작업 범위 밖. 후속 작업에서 WebMvcController/SearchController 수정으로 creator 배지 부착 필요. profile.jsp 본인 배지는 구현됨.
W3-3 SSRF 잔여 rebinding TOCTOU option-b hostname-connect 의 connect 시 재resolve 창(DNS 캐시로 최소화, 위협모델 LOW — URL 제출자=권한자). 하드닝 옵션: networkaddress.cache.ttl 설정 검토.
W3-5 배포 하드닝 (1) 업로드 저장루트 static 트리 밖 이전(LOW: static 트리 .tmp 노출 가능) (2) gameRoot 경로 이중중첩 정합(app.upload.game-storage-path=src/main/resources/static/gameresolve('game').../static/game/game). 설계 스코프-아웃(canonical 경계는 보장). dev 자산 위치 영향이라 사용자 config 결정 필요.