14 KiB
| phase | agent | agent_version | generated_at | concerns | concerns_checked | source_confidence | workers_spawned | self_verification | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| research | research-advisor | 2 | 2026-06-23T02:29:01Z |
|
true | high | 0 |
|
조사 결과 — W3-5 Unity WebGL 업로드 자동화 선행 조사
주제
bibimbap GameUpload(/api/game-files/webgl-zip) 부분구현의 선행 조사 7항목을 코드 grounding 으로 확인. 각 항목 현황(code-fact) + 정석 보강 방향 후보(해석).
본 조사는 라이브러리 API 조사가 아닌 단일 코드베이스 결합점 조사이며, 7개 항목은 사전 지정된 조사 점검표(열거형 카탈로그 아님)다. 따라서 §4.8 축-완결성 패스는 비적용. 모든 사실은 file:line 직접 확인(
확인됨), source_confidence=high. 단 stale 골자 전제 1건 concern 기록.
요약 (최상단 — 핵심 3건)
-
zip-slip 방어에 심볼릭 링크 구멍:
extractZip은target.startsWith(targetDir)정규화 검증(GameUploadController.java:266-269)으로../경로탈출은 막지만, zip 엔트리가 심볼릭 링크인 경우를 전혀 처리하지 않는다(코드 전역isSymbolicLink/NOFOLLOW/toRealPath사용 0건). 디렉터리 생성·파일 쓰기가 기존 심링크를 따라가 targetDir 밖에 쓸 수 있다(Files.createDirectories/Files.newOutputStream모두 기본 follow-links). → 정석 보강 후보: 엔트리별 심링크 차단 + 쓰기 전toRealPath재검증. — code-fact(부재) + 해석(보강) -
/game/**서빙은 실재 — 골자 QG-3 전제 stale: 골자(2026-06-17)는 "/game/**핸들러 미등록 → 서빙 미보장"이라 했으나,GameAssetController.gameAsset이@GetMapping("/game/{gameUuid}/**")로 직접 서빙한다(GameAssetController.java:31-69). UUID 검증·경로 boundary·Content-Type·CSP·Content-Encoding(br/gz) 까지 처리.UploadResourceConfig는/profile/**만 등록(UploadResourceConfig.java:22-23)한 것이 맞으나, WebGL 은 ResourceHandler 가 아니라 전용 컨트롤러로 서빙되는 의도된 설계다(버그 아님). → 골자 문구 정정 필요. — code-fact -
업로드 권한 게이트 전무: 세 업로드 엔드포인트(
webgl-zip/thumbnail/POST root) 모두 CSRF + 로그인(세션 userId) 체크만 하고 권한 검사가 없다(GameUploadController.java:113-117, 173-177, 62-65). W1 RBAC 인프라(PermissionGate/RbacInterceptor)는 실재하나InterceptorConfig가/admin/**에만 인터셉터를 건다(InterceptorConfig.java:19) →/api/game-files/**미보호. 로그인한 모든 사용자가 업로드 가능. → W1 게이트를 얹을 자리. — code-fact
포인트별 발견
포인트 1: zip-slip 방어 충분성
- 경로:
GameUploadController.java - 현황(code-fact):
- 엔트리별 정규화 후 prefix 검증:
Path target = targetDir.resolve(entry.getName()).normalize(); if (!target.startsWith(targetDir)) throw ...(266-269).../·절대경로(resolve시 절대경로면 targetDir 밖으로 normalize → startsWith 탈락)·백슬래시는 이 검증으로 차단됨. — 확인됨 - 디렉터리 엔트리:
Files.createDirectories(target)(272). 파일:Files.createDirectories(target.getParent())+copyZipEntry(277-278). 둘 다 심링크 follow 기본 동작 — NOFOLLOW/실경로 재검증 없음. — 확인됨(부재) - 심볼릭 링크 처리 코드 전무:
rg isSymbolicLink|NOFOLLOW|LinkOption|toRealPath결과 0건(전 소스). — 확인됨 %2e등 URL 인코딩 우회:extractZip은 zip 엔트리명을 디코딩하지 않고 그대로resolve하므로%2e는 리터럴 파일명이 되어 무해(zip 엔트리명은 URL 인코딩 대상 아님). 단 서빙 측GameAssetController.resolveAssetFile은UriUtils.decode(GameAssetController.java:92) 후 정규화·boundary 재검증(101-103) 하므로 서빙 경로도 boundary 보호됨. — 확인됨- NUL 바이트: 서빙 측만
assetPath.contains("\0")차단(GameAssetController.java:97). 추출 측은 미체크(JVM 이 NUL 포함 경로 쓰기 시 예외 발생하나 명시 방어 아님). — 확인됨
- 엔트리별 정규화 후 prefix 검증:
- 구멍 요약: (a) 심볼릭 링크 엔트리 무방비(최대 구멍), (b) zip 폭탄 부분방어 — 누적 압축해제 크기(512MB)·엔트리 수(8000)는 막으나 개별 엔트리 압축비(zip bomb ratio) 검사 없음(copyZipEntry 는 누적 바이트만 — 512MB 한도 내라면 통과), (c) 엔트리명 길이/중첩깊이 상한 없음. — code-fact(a,c 부재) + 해석(b)
- 정석 보강 방향 후보(해석): 엔트리별
target.toRealPath()또는 부모 디렉터리 실경로 재검증, 심링크 엔트리(entry의 external attributes 또는 추출 후Files.isSymbolicLink) 거부, 압축비 임계 검사. - 신뢰도: 확인됨
포인트 2: WebGL 빌드 포맷 검증 범위
- 경로:
GameUploadController.java:135-139, 306-316 - 현황(code-fact):
- 검증하는 것: index.html 존재만.
findIndexFile이 추출 트리를 walk 하여index.html(대소문자 무시) 중 가장 얕은 경로 1개를 선택(306-314). 없으면deleteRecursively후 400(136-139). — 확인됨 - 검증하지 않는 것:
Build/*.data·*.wasm·*.framework.js·*.loader.js등 Unity WebGL 필수 산출물 존재 검사 전무(rg 결과 해당 확장자 검증 로직 없음, contentType 매핑만 GameAssetController 에 존재). — 확인됨(부재) - index.html 내용/구조 검증 없음 — 임의 index.html 만 있어도 통과. — 확인됨
- 검증하는 것: index.html 존재만.
- 정석 보강 방향 후보(해석): Unity 빌드 마커(
Build/디렉터리 + loader/framework/wasm/data 4종 또는 그 압축변형.br/.gz) 존재 검증, index.html 의 loader 참조 정합 체크. - 신뢰도: 확인됨
포인트 3: 업로드 크기/타입
- 경로:
GameUploadController.java:40-42, 121-123, 240-252, 254-304;application.properties:15-17 - 현황(code-fact):
- 압축해제 누적 크기 상한:
WEBGL_EXTRACTED_MAX_BYTES = 512MB(40),copyZipEntry가 누적 초과 시 throw(297-299). — 확인됨 - 엔트리 수 상한:
WEBGL_MAX_ENTRIES = 8_000(41), 초과 시 throw(262-263). — 확인됨 (골자 8000 언급과 일치) - zip 원본(업로드) 크기 상한 — 코드 레벨 명시 없음. 전역
spring.servlet.multipart.max-file-size=1GB·max-request-size=1GB(application.properties:15-16) +server.tomcat.max-swallow-size=-1(무제한 swallow) 가 유일 한도. webgl-zip 핸들러는file.getSize()상한 검사 없음(썸네일만 10MB 검사 GameUploadController.java:181). — 확인됨 - Content-Type/확장자 이중 체크:
isZipFile이 MIME(application/zip·x-zip-compressed·multipart/x-zip) 또는.zip확장자 — OR 조건(둘 중 하나만 충족해도 통과, 240-252). 이중(AND) 아님. — 확인됨 - 매직바이트(zip 시그니처 PK\x03\x04) 검증 없음 — 선언 MIME/확장자만 신뢰. — 확인됨(부재)
- 압축해제 누적 크기 상한:
- 정석 보강 방향 후보(해석): webgl-zip 전용 원본 크기 상한(예: 512MB~1GB 명시), zip 매직바이트 검증, MIME+확장자 AND 강화 여부 검토.
- 신뢰도: 확인됨
포인트 4: 저장 경로 boundary
- 경로:
GameUploadController.java:44-45, 218-220, 126-130;dev/db.properties:6;application.properties:23 - 현황(code-fact):
- 설정 주입:
@Value("${app.upload.game-storage-path:src/main/resources/static}")(44), defaultsrc/main/resources/static. dev 프로파일은app.upload.game-storage-path=src/main/resources/static/game(dev/db.properties:6),spring.config.import=optional:.../db.properties(application.properties:23) 로 로드. — 확인됨 gameRoot()=Paths.get(uploadStoragePath).toAbsolutePath().normalize().resolve("game").normalize()(218-219). — 확인됨- 주의(해석): dev 설정값이 이미
.../static/game인데gameRoot()가 추가로.resolve("game")→ 실제 루트가.../static/game/game로 이중 중첩 가능. default 값(.../static)에는 맞으나 dev override 와 어긋남. 운영 배포값 미확인 — 의도/실수 판별 필요. — code-fact(경로조합) + 해석(중첩 의심) - boundary 검증: webgl-zip 은
targetDir = root.resolve(gameUuid).normalize(); if(!targetDir.startsWith(root))(127-128) — 단 gameUuid 는UUID.randomUUID()(126) 라 외부주입 아님 → 이 경로의 탈출 위험 없음. thumbnail/POST-root 는 외부 path/gameUuid 받되 normalize+startsWith 검증(199-202, 396-411). — 확인됨 - 외부 주입 경로 탈출:
uploadStoragePath자체는 운영자 설정값(외부 사용자 주입 아님). 사용자 입력 path 는 POST-root 의path파라미터뿐이며resolveTargetFile가 normalize+startsWith(root) 검증(406-409). — 확인됨
- 설정 주입:
- 정석 보강 방향 후보(해석): dev 의 이중
game중첩 정리(설정값을.../static으로 통일하거나gameRoot()의.resolve("game")제거), 운영 배포 경로 문서화. - 신뢰도: 확인됨 (단 경로 중첩의 의도 여부는 미확인 — 운영 배포값 부재)
포인트 5: /game/** 정적 핸들러 등록 (QG-3)
- 경로:
GameAssetController.java:31-69;UploadResourceConfig.java:18-24 - 현황(code-fact):
UploadResourceConfig.addResourceHandlers는/profile/**만 등록(22-23),/game/**ResourceHandler 미등록(골자 주장 일치). — 확인됨- 그러나
/game/{gameUuid}/**는 전용 컨트롤러GameAssetController.gameAsset(@GetMapping, 31)이 직접 서빙: UUID 정규화(37-41)·디렉터리 boundary(101-103)·Content-Type 매핑(130-172, wasm/js/data/json/html/css/이미지)·Content-Encoding(br/gz, 119-128)·CSP+X-Content-Type-Options+Cache-Control(57-66) 처리. — 확인됨 - → WebGL 서빙은 실제 동작하며 의도된 설계. 골자의 "서빙 미보장(QG-3, 의도/버그 미확인)" 전제는 stale — 버그 아님. — code-fact (concern 으로 격상)
- 정석 보강 방향 후보(해석): 전용 컨트롤러 방식 유지 타당(보안헤더·인코딩 협상 필요해 ResourceHandler 보다 적합). 단 ResourceHandler 미등록이 정상임을 골자에 명시.
- 신뢰도: 확인됨
포인트 6: 업로드 권한 게이트
- 경로:
GameUploadController.java:59-65, 106-117, 170-177;InterceptorConfig.java:19;security/PermissionKeys.java - 현황(code-fact):
- webgl-zip: CSRF(106-108) + 로그인(113-117)만. 권한 검사 없음. — 확인됨
- thumbnail: CSRF(170-172) + 로그인(173-177)만. — 확인됨
- POST-root(
uploadGameFiles): CSRF(59-61) + 로그인(62-65)만. — 확인됨 - W1 RBAC 실재:
PermissionGate·RbacInterceptor(클래스명 obfuscated 흔적 있으나 InterceptorConfig 는 정상RbacInterceptorimport)·PermissionKeys(GAME_JAM_MANAGE·POST_WRITE 정의). — 확인됨 InterceptorConfig.addInterceptors는registry.addInterceptor(rbacInterceptor).addPathPatterns("/admin/**")—/admin/**전용(19)./api/game-files/**미보호. — 확인됨- 업로드 전용 권한키 부재: PermissionKeys 에 GAME_UPLOAD 류 키 없음(GAME_JAM_MANAGE·POST_WRITE 만). — 확인됨(부재)
- 정석 보강 방향 후보(해석): 업로드 권한키 신설 + RbacInterceptor 경로 확장 또는 컨트롤러 내 PermissionGate 명시 호출. W1 게이트를 얹을 자리는 명확.
- 신뢰도: 확인됨
포인트 7: UUID 재사용/덮어쓰기 정책
- 경로:
GameUploadController.java:126;GameController.java:176-228, 240-269, 348-352;game-register.jsp:649-653 - 현황(code-fact):
- 모든 webgl-zip 업로드는 무조건
UUID.randomUUID()신규 생성(126) — 재업로드/편집 여부와 무관. 기존 UUID 재사용·덮어쓰기 로직 없음. — 확인됨 - 편집(edit) 흐름:
game-register.jsp는 새 zip 업로드 시 응답의 새 gameUuid/webglPath 로 hidden 필드 교체(649-653) →updateGame(GameController.java:176-228) 이 새 webgl_path 로 DB UPDATE. 기존/game/{old-uuid}/디렉터리는 삭제되지 않음 → 고아 파일. — 확인됨 - 게임 삭제:
deleteGame은softDeleteGame(soft delete) + 연관 soft-delete/like 삭제만(GameController.java:259-262). 파일시스템 정리 없음 →/game/{uuid}/영구 잔존. — 확인됨 deleteRecursively(GameUploadController.java:385) 는 업로드 실패 롤백 시에만 호출(137,155,158). 정상 교체/삭제 시 미호출. — 확인됨- 멱등성: 동일 zip 재업로드 시 매번 다른 UUID·다른 디렉터리 → 비멱등. — 확인됨
- 모든 webgl-zip 업로드는 무조건
- 정석 보강 방향 후보(해석): 편집 시 기존 UUID 유지+덮어쓰기 또는 신규 후 구버전 GC, 게임 삭제 시 자산 디렉터리 정리(soft-delete 와의 정합 — 즉시삭제 vs 유예GC), 고아 파일 배치 청소.
- 신뢰도: 확인됨
종합 판단
- 상위 패턴: 업로드 파이프라인은 "정규화 prefix 검증 + 사이즈/엔트리 상한 + 전용 서빙 컨트롤러"로 1차 골격은 갖췄으나, (1) 심링크 방어 (2) Unity 빌드 포맷 검증 (3) 권한 게이트 (4) 자산 생명주기(고아 파일) 4개 축이 비어 있다.
- 충돌/정정: 골자(2026-06-17) §코드현황5 의 "
/game/**미등록 → 서빙 미보장(QG-3)" 는 GameAssetController 실재로 뒤집힘 — 설계 진입 전 골자 문구 정정 필요(concern 기록). 골자의 "권한 체크 없음"·"zip-slip·크기상한·UUID 배치 됨" 은 코드와 일치. - 갭(설계 단계로 이월): 운영 배포 시
game-storage-path경로 중첩(static/game/game) 의도 여부, soft-delete 와 자산 즉시삭제의 정합 정책.
미해결
- 운영(prod) 프로파일의
app.upload.game-storage-path실제값 — repo 에 dev 프로파일만 존재(dev/db.properties). 경로 이중 중첩이 운영에서도 발생하는지 미확인. - 클래스명 obfuscation 흔적(
security.n등 rg 매칭) — RbacInterceptor.java 의public class n표기는 빌드/난독화 산물일 가능성. InterceptorConfig 는 정상 클래스명 사용하므로 enforcement 분석에는 영향 없음(미해결로 남김, W3-5 범위 밖).