11 KiB
11 KiB
보안 개선 체크리스트
기준 분석: 2026-06-16 프로젝트 전면 분석
상태 표시
[ ]미착수[~]진행 중[x]완료[hold]의도 확인 또는 별도 결정 필요
우선순위
| ID | 우선순위 | 항목 | 보안 영향 |
|---|---|---|---|
| B1 | P1 | login/signup CSRF 검증 추가 | MED, 완료 |
| B2 | P2 | 프로토타입 dead code 제거 | LOW-MED |
| B3 | P2 | 좋아요/댓글 서버 영속화 연결 | MED, 기능 무결성 |
| B4 | P2 | 의존성/세션/운영 하드닝 | MED |
B1. login/signup CSRF 검증 추가 (MED)
근거:
POST /signup은 CSRF 검증 없이 입력 검증과 DB 조회를 시작한다.src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:65,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:75,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:87POST /login은 CSRF 검증 없이 인증과 세션 갱신을 수행한다.src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:118,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:151,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:152- 로그인/회원가입 fetch 요청은
window.BibimbapCsrf.headers를 쓰지 않는다.src/main/webapp/WEB-INF/views/login.jsp:277,src/main/webapp/WEB-INF/views/login.jsp:279,src/main/webapp/WEB-INF/views/signup.jsp:291,src/main/webapp/WEB-INF/views/signup.jsp:293 - 공통 CSRF helper는 이미 있다.
src/main/webapp/WEB-INF/views/theme-init.jsp:19,src/main/webapp/WEB-INF/views/theme-init.jsp:27
구현 결과:
UserController.signup은 입력 검증/DB 조회 전에 CSRF를 검사한다.src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:75,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:76UserController.login은 인증/세션 갱신 전에 CSRF를 검사한다.src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:130,src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:131- 로그인/회원가입 JSP는 hidden
_csrfinput과 fetchX-CSRF-Tokenheader를 모두 제공한다.src/main/webapp/WEB-INF/views/login.jsp:221,src/main/webapp/WEB-INF/views/login.jsp:283,src/main/webapp/WEB-INF/views/signup.jsp:219,src/main/webapp/WEB-INF/views/signup.jsp:297 - 회귀 테스트는 토큰 없는 요청 거부, 정상 토큰 signup 성공, 정상 토큰 login 성공, 정상 토큰 invalid login 흐름을 검증한다.
src/test/java/com/pandoli365/bibimbap/controller/api/UserControllerCsrfTest.java:35,src/test/java/com/pandoli365/bibimbap/controller/api/UserControllerCsrfTest.java:54,src/test/java/com/pandoli365/bibimbap/controller/api/UserControllerCsrfTest.java:71,src/test/java/com/pandoli365/bibimbap/controller/api/UserControllerCsrfTest.java:101,src/test/java/com/pandoli365/bibimbap/controller/api/UserControllerCsrfTest.java:136
체크리스트:
UserController.signup시작부에서CsrfTokens.isValid(request)를 먼저 검사한다.UserController.login시작부에서CsrfTokens.isValid(request)를 먼저 검사한다.- JSON 요청은
403과 동일한AuthResult형태로 응답한다. - 일반 form fallback은
/signup?error=csrf또는/login?error=csrf리다이렉트를 사용한다. login.jspfetch headers에window.BibimbapCsrf.headers(...)를 적용한다.signup.jspfetch headers에window.BibimbapCsrf.headers(...)를 적용한다.- JS 비활성 또는 일반 form submit을 고려해 hidden
_csrfinput을 추가한다. POST /login토큰 없음: 403 테스트를 추가한다.POST /signup토큰 없음: 403 테스트를 추가한다.- 정상 토큰이 있는 login/signup 성공 테스트를 추가한다.
- 로그인 성공 시
request.changeSessionId()동작을 유지한다. - 회원가입 중복 이메일 응답과 기존 error code 흐름이 깨지지 않도록 기존 분기 뒤에 CSRF만 선행 배치한다.
완료 조건:
- 토큰 없는 login/signup POST가 실패한다.
- 토큰 있는 login/signup POST가 기존과 동일하게 성공한다.
- 기존 CSRF 적용 엔드포인트의 응답 형태가 바뀌지 않는다.
- 보안 테스트가 로컬
mvn test에서 실행된다.
B2. 프로토타입 dead code 제거
근거:
abstracts.Service는 현재 컨트롤러 흐름과 별개로 수동 인증/요청 검증을 제공한다.src/main/java/com/pandoli365/bibimbap/abstracts/Service.java:5,src/main/java/com/pandoli365/bibimbap/abstracts/Service.java:7,src/main/java/com/pandoli365/bibimbap/abstracts/Service.java:11header.jspf는 Spring Security taglib와${_csrf}를 전제로 한다.src/main/webapp/WEB-INF/jsp/fragments/header.jspf:1,src/main/webapp/WEB-INF/jsp/fragments/header.jspf:2,src/main/webapp/WEB-INF/jsp/fragments/header.jspf:14- 실제 화면은
/WEB-INF/views/header.jsp를 include한다.src/main/webapp/WEB-INF/views/login.jsp:203,src/main/webapp/WEB-INF/views/signup.jsp:201 GameCatalog는 빈 배열 기반 fallback이다.src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:8,src/main/java/com/pandoli365/bibimbap/game/GameCatalog.java:18GameController는 DB 게임이 없으면GameCatalogfallback을 시도한다.src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:111,src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:116
체크리스트:
rg "abstracts|GameCatalog|header.jspf"로 실제 참조를 재확인한다.abstracts패키지를 삭제해도 컴파일이 깨지지 않는지 확인한다.header.jspf삭제 전 JSP include 경로가 전부/WEB-INF/views/header.jsp인지 확인한다.GameCatalogfallback 제거 시 없는 게임 ID의 기대 동작을redirect:/또는 404로 결정한다.- 삭제 PR에는 기능 변경이 없도록 테스트와 수동 확인 범위를 좁힌다.
- 문서에서 제거된 프로토타입 흐름을 최신 구조로 갱신한다.
완료 조건:
- dead code 파일이 제거되거나 “보존 이유”가 문서화된다.
mvn test또는 최소 컴파일 검증이 통과한다.- 없는 게임 상세 접근의 동작이 명확하다.
B3. 좋아요/댓글 서버 영속화 연결
근거:
- 좋아요 매퍼는 있다.
src/main/java/com/pandoli365/bibimbap/mapper/GameLikesMapper.java:10,src/main/java/com/pandoli365/bibimbap/mapper/GameLikesMapper.java:24 - 댓글 매퍼는 있다.
src/main/java/com/pandoli365/bibimbap/mapper/GameCommentsMapper.java:10,src/main/java/com/pandoli365/bibimbap/mapper/GameCommentsMapper.java:27 - 게임 삭제 시 댓글/좋아요 데이터 정리 로직도 있다.
src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:243,src/main/java/com/pandoli365/bibimbap/controller/api/GameController.java:244 - 현재 UI는 좋아요와 댓글을
localStorage에만 저장한다.src/main/webapp/WEB-INF/views/game-detail.jsp:812,src/main/webapp/WEB-INF/views/game-detail.jsp:830,src/main/webapp/WEB-INF/views/game-detail.jsp:913,src/main/webapp/WEB-INF/views/game-detail.jsp:928
의도 확인:
- [hold] 좋아요를 로그인 사용자만 허용할지, 익명 사용자 키 기반으로 허용할지 결정한다.
- [hold] 댓글을 로그인 사용자만 허용할지, 익명 닉네임 댓글을 허용할지 결정한다.
- [hold] 기존 localStorage 댓글/좋아요를 서버로 마이그레이션할지, 신규 서버 데이터로만 전환할지 결정한다.
체크리스트:
POST /game/{id}/like또는/api/games/{id}/like엔드포인트를 설계한다.- 좋아요 추가/취소는 CSRF 검증을 적용한다.
game_likes중복 방지 키를 DB 또는 트랜잭션에서 보장한다.games.like_count증감은 race condition 없이 처리한다.GET /game/{id}/comments또는 상세 모델 주입 방식을 결정한다.POST /game/{id}/comments는 CSRF, 길이 제한, 작성자 정책을 적용한다.- 댓글 삭제는 작성자 또는 관리자 권한을 확인한다.
- 서버에서 내려온 댓글도 JSP escape 또는 DOM
textContent로 렌더링한다. - 남용 방지를 위해 rate limit, 로그인 제한, 운영 신고/삭제 정책 중 최소 한 가지를 결정한다.
- localStorage UI는 서버 응답 기준으로 교체한다.
완료 조건:
- 새로고침/브라우저 변경 후에도 좋아요와 댓글이 유지된다.
- 토큰 없는 좋아요/댓글 변경 요청이 실패한다.
- XSS payload 댓글이 스크립트로 실행되지 않는다.
- 게임 삭제 시 관련 댓글/좋아요 정리가 유지된다.
B4. 의존성/세션/운영 하드닝
근거:
- Spring Boot가 SNAPSHOT이다.
pom.xml:29 - snapshot repository와 pluginRepository가 활성화되어 있다.
pom.xml:200,pom.xml:210 - 세션 쿠키 하드닝 설정이 없다.
src/main/resources/application.properties:1,src/main/resources/application.properties:27 - MyBatis TRACE 로그가 켜져 있다.
src/main/resources/application.properties:25,src/main/resources/application.properties:26,src/main/resources/application.properties:27 - multipart 상한이 1GB다.
src/main/resources/application.properties:15,src/main/resources/application.properties:16,src/main/resources/application.properties:17,src/main/resources/application.properties:18
체크리스트:
- Spring Boot를 안정 release 버전으로 고정한다.
- snapshot repository와 pluginRepository 필요성을 제거하거나 문서화한다.
server.servlet.session.cookie.http-only=true를 설정한다.- live 프로필에서
server.servlet.session.cookie.secure=true를 설정한다. server.servlet.session.cookie.same-site=lax또는 stricter 정책을 결정한다.- 운영 로그에서 MyBatis TRACE를 낮추고 민감 파라미터 노출 가능성을 확인한다.
- multipart 상한을 실제 운영 허용치와 맞춘다.
- 의존성 CVE 스캔 도구를 정한다. 후보: Dependabot, OWASP Dependency-Check, Maven Versions Plugin.
- CVE 스캔 결과와 예외 처리 기준을
docs/security/에 기록한다.
완료 조건:
- 빌드가 snapshot repository 없이 재현 가능하다.
- 운영 쿠키에 HttpOnly/Secure/SameSite 정책이 반영된다.
- CVE 스캔 결과가 문서화된다.
- 운영 로그 레벨이 민감 데이터 노출을 최소화한다.
공통 테스트 매트릭스
- CSRF: 모든 상태 변경 엔드포인트가 토큰 없는 요청을 거부한다.
- SQLi: 검색어와 인증 입력에 특수 문자를 넣어도 쿼리 구조가 변하지 않는다.
- 업로드:
../, 절대 경로, nested zip, 과대 zip을 거부한다. - XSS: 게임 이름, 제작자 한마디, 댓글, 검색어가 HTML로 실행되지 않는다.
- 권한: 게임 수정/삭제, 댓글 삭제, 프로필 변경은 소유자 또는 로그인 조건을 확인한다.