백엔드갈아엎기 시리즈초기 기록
Spring Boot 첫 배포에 깔린 보안 빚 — POP-SPOT v1.0 회고
동
김동현GCP VM · Oracle · Gemini Free 로 빠르게 띄운 첫 배포. 시크릿 평문 커밋, JWT default_secret, CORS *, BCrypt 10 — v1.1 에서 한 번에 갚을 기술 빚을 정리한다.
넥스트아이티 교육센터에서 Spring Boot 와 Next.js 를 배우면서 시작한 첫 배포다. "일단 가입·로그인·팝업 조회만 돌면 된다" 는 기준으로 빠르게 만들었고, 그 대가로 다음 버전에서 한 번에 갚게 될 기술 빚을 가득 안고 띄웠음.
이 글에서 다루는 것
모르는 단어 한 줄로
| 용어 | 한 줄 설명 |
|---|---|
| JWT | 로그인 한 사람이라는 걸 증명하는 짧은 문자열. 사이트 출입증과 같음 |
| CORS | 브라우저가 "이 사이트가 다른 도메인 호출해도 되냐" 미리 묻는 규칙 |
| BCrypt | 비밀번호를 원본 그대로 저장하지 않고 한 방향으로 뒤섞는 함수. strength 가 클수록 깨기 어렵다 |
| Sequence (Oracle) | "다음 번호" 를 발급해주는 DB 객체. INSERT 때마다 손으로 가져와 넣어줘야 한다 |
| RPM | Requests Per Minute. 1 분에 보낼 수 있는 요청 수. 무료 AI 의 한도 단위로 자주 쓰인다 |
무엇이 바뀌었나
v1.0 은 "처음 만든 것" 이라 비교할 이전 버전이 없다. 어떻게 생긴 시스템이었는지만 정리한다.
| 레이어 | v1.0 |
|---|---|
| 프론트 | Vercel · Next.js |
| 백엔드 | GCP Compute Engine · Spring Boot 3.x · 8080 포트 직접 노출 |
| DB | Oracle (Sequence 직접 관리) |
| AI | Gemini Free — 분당 10 회 (RPM 10) |
| 인증 | JWT (시크릿 default_secret) · OAuth2 (카카오·네이버·구글) |
| 비밀번호 | BCrypt strength 10 |
| 관측 | 없음. System.out 과 콘솔만 |
스택 선정 이유
코드로는 어떻게 (필요한 부분만)
가장 위험했던 건 시크릿 평문이었다.
javascript
# v1.0 — application.properties 에 평문 시크릿. 깃 커밋까지 같이 됨
jwt.secret=default_secret
# ^^^^^^^^^^^^^^ 라이브러리 예제의 디폴트 값. 같은 디폴트를 쓰는 누구도
# 동일한 토큰을 만들 수 있음 → 사실상 인증 없음
gemini.api-key=AIzaSy_실제키_평문
# ^^^^^^^^^^^^^^^^^^^^^ 진짜 키를 그대로 텍스트로 적음
# 깃에 올라가는 시각 Google 스캐너가 탐지
spring.datasource.password=1234
# ^^^^ DB 비밀번호. 레포 볼 수 있는 모든 사람에게 노출JWT 의 default_secret 은 라이브러리 예제 코드의 디폴트다. 즉 같은 디폴트를 쓰는 누구든 같은 토큰을 만들 수 있었다는 뜻.
Oracle 의 Sequence 는 INSERT 할 때 직접 가져와 채워줘야 한다. 안 채우면 NULL 이 들어가서 ORA-01400 에러가 난다.
파일: Oracle SQL·Plus / DataGrip 에서 직접 실행한 쿼리 (코드 파일 아닌 DB 콘솔 용)
sql
-- v1.0 — Oracle Sequence 를 손으로 다루는 구조
SELECT chat_msg_seq.NEXTVAL FROM dual;
-- ^^^^^^^^^^^^^ ^^^^ 한 행을 돌려주는 Oracle 의 더미 테이블
-- | (SELECT 에 FROM 절 필수라 인위적으로 존재)
-- +-- chat_msg_seq 가 있다면 다음 숫자를 발급해 준다 (예: 7)
-- 그런 다음 그 7 을 가지고 INSERT 의 id 컬럼에 직접 넣어야 함
-- INSERT INTO chat_messages (id, content) VALUES (7, '안녕');
-- 근데 id 안 넣고 바로 INSERT 하면:
-- INSERT INTO chat_messages (content) VALUES ('안녕');
-- → ORA-01400: cannot insert NULL into ("PK"... id)
-- ↑ id 에 NULL 이 들어갈 수 없다. PK 는 NOT NULLv1.1 에서 PostgreSQL 의 IDENTITY (자동 채번) 로 바뀐다.
비하인드 · 사고
수료 과제 발표 끝나고 며칠 뒤, Google 의 시크릿 스캐너가 깃에 올라간 Gemini 키를 자동 감지했음. 그 결과 프로젝트 전체 quota 가 0 으로 강제 변경됨. 새 키를 발급해도 같은 프로젝트면 차단이 풀리지 않았다. v1.1 에서 결국 Groq 로 LLM 통째 이사를 가게 된 직접 원인이 이거다.
교훈 한 줄. "동작했다" 와 "안전하다" 는 다른 말이다. v1.0 은 동작은 했고, 안전하진 않았다. 첫 배포 때 가장 흔히 빠지는 함정이라고 생각함.
관련 글
관련 글
인트로 페이지 제거 → 루트가 메인 직행 + AuthGuard 스피너 게이트 제거로 SEO 색인 해결 — POP-SPOT v2.23 (+v2.23.1/2)발견형 서비스인데 모든 방문자가 5 섭션 인트로→ENTER 거쳐야 메인 도달. 이탈을 키우는 구조라 제거. v2.23.1·2 은 SEO 색인 이시합 해결 — /popups/[slug] 이 크롤러를 /login 으로 튕긴게 수정하고, AuthGuard 의 스피너 게이트를 제거해 공개 페이지가 실제 HTML로 서버 렌더링 되도록.전면 보안 감사 + 수정 — IDOR 5 곳 · 저장형 XSS · 메모리/스토리지 누수 · 클린코드 점검 — POP-SPOT v2.22인증·인가·인젝션·암호화·프론트·컴플라이언스 6 영역 병렬 감사. C1 IDOR (Stamp/MyPage/Mate/ChatFile) + C2 저장형 XSS (카카오 로드뷰 오버레이) + H1 인증 없는 업로드 + H2/H3 무한 증가 인메모리 맵 + H4 SSE emitter 누수 + H5 토큰 노출. 동시에 운영 점검 4 건 (신고 어뻐징 · 크롤링 입력 정제 · 이메일 열거 · 백업 하드닝).Spotify OAuth + Web Playback SDK 통합 — 3-tier 재생 엔진 (Premium SDK / iTunes preview / YouTube) — POP-SPOT v2.21 S10~S18Spotify OAuth 백엔드(AES-256-GCM 토큰 암호화) + Web Playback SDK 프론트 통합 + iTunes preview 폴백 + YouTube 폴백의 3-tier 재생 엔진. Premium 은 320kbps 풀트랙, Free/미연결은 30~90초 preview, 외엔 YouTube 폴백. S17/S18 에서 운영 버그 네 건 추가 수정.