Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .claude/agents/security-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
---
name: security-reviewer
description: Coming Backend의 인증·인가 관련 변경사항(JWT, OAuth2, Redis 토큰 처리)을 Coming 도메인 정책 기준으로 검토하는 보안 리뷰 에이전트. "auth 관련 코드 작성 후 보안 리뷰해줘", "JWT/OAuth2 변경사항 보안 검토해줘" 요청 시 사용한다. 읽기 전용이며 코드를 직접 수정하지 않는다.
tools: Read, Bash, Grep
---

# Coming Backend 보안 리뷰 에이전트

변경된 Java 코드를 아래 체크리스트 기준으로 검토하고 심각도별 이슈를 보고한다.
**읽기 전용 리뷰어다 — 코드를 직접 수정하지 않는다. 발견한 이슈는 보고만 한다.**

## 실행 순서

1. 변경된 파일 목록과 diff를 확인한다 (브랜치 커밋분 + 작업 트리 + 신규 파일)
```bash
BASE=$(git merge-base origin/develop HEAD)
git diff --name-only "$BASE" # 브랜치 커밋분 + 작업 트리 변경
git diff "$BASE"
git ls-files --others --exclude-standard # untracked 신규 파일 — 목록의 파일은 직접 읽는다
```
2. 아래 체크리스트 기준으로 검토한다. 인증/인가와 무관한 변경이면 해당 없음으로 보고한다.
3. 심각도별로 이슈를 정리해 보고한다.

---

## Coming 인증 정책 (기준)

- Access Token: 30분, `Authorization: Bearer {token}` 헤더로만 전달
- Refresh Token: 7일, HttpOnly Cookie로만 전달 (JS에서 접근 불가해야 함)
- 로그아웃 시 Access Token을 Redis 블랙리스트에 등록
- OAuth2 Provider: Google, Kakao만 지원

## 심각도 기준

| 심각도 | 의미 |
|--------|------|
| 🔴 critical | 즉시 수정 필요 (인증 우회, 토큰 탈취, 권한 상승 가능) |
| 🟡 warning | 개선 권장 (정책 불일치, 방어 계층 누락) |
| 🔵 suggestion | 선택적 개선 |

## 검토 체크리스트

### 🔴 critical
- 신규/변경 엔드포인트에 `@PreAuthorize` 또는 SecurityConfig 경로 설정 누락 (인증 필요 API가 허용 목록에 포함)
- `ROLE_ADMIN` 필요 엔드포인트에 권한 체크 누락 (일반 사용자가 403 없이 접근 가능)
- Refresh Token을 응답 바디·헤더·로그에 노출 (HttpOnly Cookie 외 경로로 전달)
- Access Token을 쿠키에 저장하거나 Refresh Token을 로컬 스토리지·바디로 내려보내는 등 Bearer/Cookie 역할 뒤바뀜
- 로그아웃/토큰 폐기 로직에서 Redis 블랙리스트 등록 누락
- JWT 서명 검증 없이 payload를 신뢰 (예: 파싱만 하고 `verify` 생략)
- 사용자 입력(닉네임, 문의 내용 등)을 파라미터 바인딩 없이 쿼리 문자열에 직접 결합 (SQL Injection) — JPA 파라미터 바인딩·엔티티 필드 대입은 해당 없음
- 비밀번호·API 키·클라이언트 시크릿 등이 코드에 하드코딩되거나 로그에 평문 출력

### 🟡 warning
- 신규 인증 필요 엔드포인트에 Rate Limit 필터 미적용
- OAuth2 실패·인증 예외 처리 시 내부 스택트레이스나 시스템 정보를 그대로 클라이언트에 노출
- 다른 사용자의 리소스에 접근 가능한지(IDOR) 확인하는 소유자 검증 로직 누락 (예: `concertId`만으로 타 유저의 캘린더 항목 삭제 가능)
- 민감 정보(email, provider_id 등)를 응답 DTO에 불필요하게 포함
- `application.yaml`에 `${ENV_VAR:default}` 형식이 아닌 민감 정보 직접 기재

### 🔵 suggestion
- 인증 관련 로그 레벨이 CLAUDE.md 기준(WARN: OAuth2 실패, INFO: 로그인 성공 등)과 다르게 기록됨
- 에러 메시지가 공격자에게 유효한 계정/토큰 존재 여부를 암시 (예: "존재하지 않는 사용자"와 "비밀번호 불일치"를 구분해서 노출)

---

## 결과 보고

```
=== 보안 리뷰 결과 ===
검토 파일: {파일 목록}
인증/인가 관련 변경: 있음 | 없음

🔴 critical: {N}건
🟡 warning: {N}건
🔵 suggestion: {N}건

{이슈 상세 목록 — 파일:라인, 문제, 개선 방향}

{이슈 없으면: "✅ 보안 관점에서 커밋 진행 가능"}
{🔴 있으면: "🚫 커밋 전 critical 이슈를 수정하세요"}
```
70 changes: 70 additions & 0 deletions .claude/hooks/guard_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""PreToolUse: 시크릿 파일 접근과 커밋된 Flyway 마이그레이션 수정을 차단한다.

Bash는 명령을 토큰으로 나눠 각 토큰의 파일명을 검사한다. 따옴표로 묶인 문장(공백 포함 토큰)과
heredoc 본문은 제외하므로 커밋 메시지·PR 본문의 언급은 걸리지 않는다.
문자열 조립 같은 의도적 우회까지 막지는 않는다 — 실수 방지용이다.
"""
import json
import os
import re
import shlex
import subprocess
import sys

SECRET_NAME = re.compile(r"^(\.env(\..+)?|application-(local|secret).*|credentials(\..+)?|.*\.secrets?)$")
SECRET_ALLOWED = {".env.example"}
HEREDOC = re.compile(r"<<-?\s*(['\"]?)(\w+)\1.*?\n(.*?)^\s*\2\s*$", re.S | re.M)
REDIRECT_PREFIX = re.compile(r"^[0-9&]*[<>|]+")


def deny(reason):
print(json.dumps({"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": reason,
}}, ensure_ascii=False))
sys.exit(0)


def is_secret(path):
name = os.path.basename(path.rstrip("/"))
return bool(SECRET_NAME.match(name)) and name not in SECRET_ALLOWED


def bash_targets(command):
command = HEREDOC.sub("", command)
lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|<>()")
lexer.whitespace_split = True
try:
tokens = list(lexer)
except ValueError:
tokens = command.split()
# 파일명에 공백이 든 토큰은 따옴표로 묶인 문장이므로 제외(디렉터리 공백은 허용), `--file=.env`는 `=` 뒤만 본다
tokens = [t for t in tokens if not re.search(r"\s", os.path.basename(t))]
return [REDIRECT_PREFIX.sub("", t).rsplit("=", 1)[-1] for t in tokens]


data = json.load(sys.stdin)
tool = data.get("tool_name", "")
tool_input = data.get("tool_input", {})
path = tool_input.get("file_path", "")
name = os.path.basename(path)

if tool == "Bash":
targets = bash_targets(tool_input.get("command", ""))
elif tool == "Grep":
targets = [tool_input.get("path", ""), tool_input.get("glob", "")]
else:
targets = [path]
hits = [t for t in targets if t and is_secret(t)]
if hits:
deny(f"시크릿 파일 접근 차단: {', '.join(hits)} — 필요한 내용은 사용자에게 직접 수정을 요청한다")

if tool in ("Write", "Edit") and "/db/migration/" in path and name.startswith("V"):
cwd = data.get("cwd") or "."
committed = subprocess.run(
["git", "cat-file", "-e", f"HEAD:./{os.path.relpath(path, cwd)}"],
cwd=cwd, capture_output=True,
).returncode == 0
if committed:
deny(f"커밋된 Flyway 마이그레이션 수정 차단: {path} — 변경은 새 V{{n}}__*.sql 파일로 추가한다")
5 changes: 5 additions & 0 deletions .claude/hooks/stop_reminder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/sh
# Stop: 커밋되지 않은 Java 변경이 있을 때만 커밋 전 워크플로우를 안내한다.
cd "$CLAUDE_PROJECT_DIR" 2>/dev/null || exit 0
[ -n "$(git status --porcelain -- '*.java')" ] || exit 0
echo '{"systemMessage": "⚠️ 커밋되지 않은 Java 변경 있음 — write-tests → ./gradlew test → /be-review (auth 변경 시 + security-reviewer) → /simplify → /commit → /pr"}'
10 changes: 5 additions & 5 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"matcher": "Read|Write|Edit|Grep|Bash",
"hooks": [
{
"type": "command",
"command": "python3 -c \"import json,sys; data=json.load(sys.stdin); fp=data.get('tool_input',{}).get('file_path',''); blocked=['.env','.secret','application-prod','application-local','credentials','application-secret']; blocked_match=next((b for b in blocked if b in fp),None); print(json.dumps({'hookSpecificOutput':{'hookEventName':'PreToolUse','permissionDecision':'deny','permissionDecisionReason':'시크릿 파일 수정 차단: '+fp}})) if blocked_match else None\"",
"statusMessage": "시크릿 파일 검사 중..."
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/guard_files.py\"",
"statusMessage": "보호 파일 검사 중..."
}
]
}
Expand All @@ -18,11 +18,11 @@
"hooks": [
{
"type": "command",
"command": "echo '⚠️ 커밋 전 워크플로우: /be-review → /simplify → /commit → /pr'"
"command": "sh \"$CLAUDE_PROJECT_DIR/.claude/hooks/stop_reminder.sh\""
}
]
}
],
"PostToolUse": []
}
}
}
Binary file added .github/erd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 11 additions & 39 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| Cache/Session | Redis |
| Auth | OAuth2 (Google/Kakao) + JWT |
| ORM | Spring Data JPA |
| Batch / Mail | Spring Batch, Spring Mail + Thymeleaf |
| Util | Lombok |
| Build | Gradle |

Expand All @@ -24,7 +25,12 @@ com.Coming.Backend
├── concert/ # 공연, 예매 링크, 셋리스트
├── calendar/ # 사용자 공연 캘린더
├── release/ # 음악 발매 (앨범, 트랙)
├── user/ # 사용자 정보
├── rating/ # 공연·발매 별점
├── post/ # 커뮤니티 게시글·댓글, 멘션 태그, 통합 검색
├── report/ # 게시글·댓글 신고
├── notice/ # 공지사항
├── policy/ # 약관·개인정보처리방침, 개정 안내 메일(Spring Batch)
├── user/ # 마이페이지 (다가오는 공연, 관람 이력, 내 문의)
├── inquiry/ # 문의
├── admin/ # 관리자
└── common/
Expand Down Expand Up @@ -58,7 +64,8 @@ com.Coming.Backend

## 알려진 제약

- `application-local.yaml`은 gitignore 대상이며, 프로젝트 훅이 파일명에 `application-local`/`application-prod`/`.env`/`credentials`가 포함된 파일의 Write/Edit를 자동 차단한다 (`.claude/settings.json`). 이 파일 수정이 필요하면 Claude가 직접 편집할 수 없으니, 추가할 내용을 알려주고 사용자가 직접 추가하도록 요청한다.
- `application-local.yaml`은 gitignore 대상이며, 프로젝트 훅(`.claude/hooks/guard_files.py`)이 시크릿 파일(`.env*`(`.env.example` 제외)·`application-local*`·`application-secret*`·`credentials*`·`*.secret(s)`)의 Read/Write/Edit/Grep 및 Bash 명령 내 접근을 자동 차단한다. 이 파일 수정이 필요하면 Claude가 직접 편집할 수 없으니, 추가할 내용을 알려주고 사용자가 직접 추가하도록 요청한다.
- 같은 훅이 git에 커밋된 Flyway 마이그레이션(`db/migration/V*.sql`)의 수정도 차단한다 (checksum 불일치 방지). 스키마 변경은 항상 새 버전 파일로 추가한다.

## 명세 위치 (Cominggg/Specification)

Expand Down Expand Up @@ -107,45 +114,10 @@ com.Coming.Backend
### 커밋 전 체크리스트

- `/be-review` 통과(🔴 critical 0건) 전에 `/commit`을 실행하지 않는다.
- auth 관련 코드(JWT, OAuth2, Redis 토큰 처리) 작성 시 `/security-review`도 추가 실행한다.
- auth 관련 코드(JWT, OAuth2, Redis 토큰 처리) 작성 시 `security-reviewer` 에이전트도 추가로 호출한다 (Coming 인증 정책 기준 전용 체크리스트 보유, 읽기 전용).

---

## 행동 원칙

> Adapted from [andrej-karpathy-skills/CLAUDE.md](https://github.com/forrestchang/andrej-karpathy-skills/blob/main/CLAUDE.md)
> These guidelines bias toward caution over speed. For trivial tasks, use judgment.

### 1. 코딩 전에 먼저 생각하라

- 가정을 명시적으로 밝혀라. 불확실하면 물어봐라.
- 여러 해석이 가능하면 모두 제시하고, 조용히 하나를 고르지 마라.
- 더 단순한 방법이 있으면 말해라. 필요하면 반박해라.
- 무언가 불분명하면 멈춰라. 무엇이 헷갈리는지 이름 붙이고 물어봐라.

### 2. 단순함 우선

- 요청된 것 이상의 기능을 만들지 마라.
- 단일 사용 코드에 추상화를 만들지 마라.
- 요청되지 않은 유연성이나 설정 가능성을 넣지 마라.
- 불가능한 시나리오에 대한 에러 핸들링을 만들지 마라.
- 200줄로 쓴 코드가 50줄로 가능하면 다시 써라.

### 3. 외과적 변경

- 요청된 코드만 수정하라. 인접한 코드, 주석, 포맷을 "개선"하지 마라.
- 망가지지 않은 것을 리팩터링하지 마라.
- 기존 스타일이 마음에 들지 않아도 맞춰라.
- 관련 없는 데드코드를 발견하면 언급만 하고, 삭제하지 마라.

### 4. 목표 기반 실행

작업을 검증 가능한 목표로 변환하라:
- "검증 추가" → "잘못된 입력 테스트 작성 후 통과"
- "버그 수정" → "재현 테스트 작성 후 통과"

다단계 작업은 계획을 먼저 제시하라:
```
1. [단계] → 검증: [체크]
2. [단계] → 검증: [체크]
```
전역 `~/.claude/CLAUDE.md`의 "행동 원칙"(코딩 전에 먼저 생각하라·단순함 우선·외과적 변경·목표 기반 실행)을 따른다.
Loading
Loading