32장. 인증과 보안 — Token · Secret · Injection · 개인정보
보안 결함에는 다른 결함과 구별되는 성질이 하나 있다.
조용하다.
권한 체크가 빠진 API는 테스트를 통과한다.
빌드도 되고, 응답도 정상이고, 아무도 모른다.
누군가 찾아낼 때까지.
Agent가 만드는 코드에서
이 성질이 특히 위험해진다.
가장 흔한 실패는 권한 누락이다
26장에서 API를 복제할 때 이런 일이 생긴다.
// 참고한 API
@GetMapping("/orders/{id}")
@PreAuthorize("@orderAuth.canRead(#id)")
fun getOrder(@PathVariable id: Long) = ...
// Agent가 만든 API
@GetMapping("/orders/{id}/payments")
fun getPayments(@PathVariable id: Long) = ... // 권한 체크 없음
⚠️ 애노테이션 하나가 빠졌다.
기능은 완벽하게 동작한다.
남의 주문 결제 내역도 아주 잘 조회된다.
이것을 막는 방법은 셋이다.
# CLAUDE.md
- 모든 조회·수정 API는 리소스 소유자 검증을 거친다
- 신규 엔드포인트는 `@PreAuthorize` 또는 명시적 권한 검사가 필수다
- 권한 검사를 생략해야 하는 경우 이유를 주석으로 남긴다
# 작업 지시
이 API에 필요한 권한 검사를 명시해줘.
참고한 API와 권한 정책이 같아야 해.
// 테스트
@Test
fun `다른 사용자의 주문 결제 내역은 조회할 수 없다`() { ... }
🔥 세 번째가 가장 강하다.
권한 테스트는 API마다 한 건씩 쌓아두면
Agent가 새 API를 만들 때 같은 패턴을 복제한다.
시크릿은 Context에 넣지 않는다
7장에서 deny 목록에 넣은 이유다.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./src/main/resources/application-prod.yml)",
"Read(**/*credentials*)",
"Bash(env)",
"Bash(printenv:*)"
]
}
}
한 번 읽히면 그 세션 내내 Context에 남는다.
대화 기록에도 남는다.
⚠️ 그리고 Agent가 만든 코드에서도 확인한다.
이번 변경에서 다음이 없는지 확인해줘.
- 하드코딩된 키·토큰·비밀번호
- 테스트 코드의 실제 자격증명
- 설정 파일에 추가된 평문 값
- 로그에 시크릿이 찍히는 코드
두 번째가 자주 나온다.
Agent가 테스트를 만들면서"test-api-key-abc123" 같은 값을 넣는데,
그게 실제 개발 환경 키인 경우가 있다.
Injection
Agent는 대개 파라미터 바인딩을 쓴다.
문제는 레거시를 흉내 낼 때다.
// 기존 코드에 이런 게 있으면
val sql = "SELECT * FROM orders WHERE status = '$status'"
// Agent가 같은 스타일로 새 쿼리를 만든다
26장에서 말한 그 문제다.
지목한 파일이 나쁘면 나쁜 패턴이 복제된다.
- 네이티브 쿼리에 문자열 연결을 쓰지 않는다
기존 코드에 있어도 새 코드는 파라미터 바인딩을 쓴다
- 동적 정렬·필터는 화이트리스트로 검증한다
두 번째가 놓치기 쉽다.
// 정렬 컬럼을 그대로 받으면 SQL이 뚫린다
fun findOrders(sortBy: String) =
jdbcTemplate.query("SELECT * FROM orders ORDER BY $sortBy")
개인정보가 새는 세 곳
| 경로 | 흔한 사고 |
|---|---|
| 로그 | 디버깅용 log.info("request: $request") |
| 응답 | 엔티티 직접 반환 시 불필요한 필드 노출 |
| 테스트 fixture | 실제 고객 데이터를 복사해 넣음 |
첫 번째가 압도적으로 많다.
24장에서 로그를 피드백으로 쓰라고 했는데,
그 로그에 주민번호가 들어가면 안 된다.
- 요청·응답 객체를 통째로 로그에 찍지 않는다
- 개인정보(이름·연락처·주소·카드번호)는 마스킹한다
- 마스킹 유틸은 `common/log/Masking.kt` 를 쓴다
두 번째는 26장의 “엔티티 직접 반환” 과 같은 뿌리다.
한 규칙이 두 문제를 막는다.
보안 리뷰는 별도 세션으로
18장에서 구현 세션과 Review 세션을 나눴다.
보안은 그 분리가 특히 중요하다.
구현 세션에는
“이렇게 하기로 했다” 는 합의가 쌓여 있다.
같은 Context에서 보안 검토를 시키면
그 합의를 근거로 “문제 없습니다” 를 확인한다.
# 새 세션에서
@.git 의 최근 커밋 3개 Diff만 읽고 보안 관점에서 검토해줘.
확인할 것:
- 인증·인가 검사 누락
- 입력 검증 누락
- 시크릿 하드코딩
- 개인정보 로깅·노출
- SQL/커맨드 injection
- 권한 상승 가능 경로
각 항목마다 확인했음/문제있음 을 명시하고,
문제는 파일과 줄 번호를 함께 알려줘.
⚠️ 마지막 요구가 중요하다.
“특별한 문제가 없습니다” 만 오면
검토를 한 것인지 알 수 없다.
항목별로 답하게 하면 빠뜨린 것이 보인다.
이 요청은 48장에서 Skill로 만들기 좋은 형태다.
Agent에게 맡기지 않는 것
이 영역에는 위임 금지선이 있다.
| 맡기지 않는다 | 이유 |
|---|---|
| 인증 방식 설계 | 틀리면 전면 재작업 |
| 암호화 알고리즘 선택 | 직접 구현은 특히 금지 |
| 권한 모델 정의 | 도메인 판단 |
| 시크릿 로테이션 | 되돌릴 수 없다 |
| 보안 취약점 대응 배포 | 사람이 검증 |
Agent가 잘하는 것은 다른 쪽이다.
- 누락 찾기 (권한 체크 없는 엔드포인트 목록)
- 패턴 검색 (문자열 연결 쿼리 전수 조사)
- 일관성 확인 (같은 리소스인데 정책이 다른 곳)
보안에서 Agent는 설계자가 아니라
아주 성실한 점검자다.
이 장의 핵심
- 보안 결함은 조용하다 — 테스트도 통과하고 응답도 정상이다
- 가장 흔한 실패는 API 복제 과정의 권한 검사 누락이다
- 권한 테스트를 API마다 쌓아두면 Agent가 그 패턴을 복제한다
- 시크릿은 한 번 읽히면 세션 내내 Context와 대화 기록에 남는다
- 테스트 코드에 실제 개발 환경 키가 들어가는 사고가 자주 난다
- 나쁜 레거시 패턴을 지목하면 injection 취약점이 복제된다
- 동적 정렬·필터 컬럼은 화이트리스트로 검증한다
- 개인정보는 로그·응답·테스트 fixture 세 곳으로 샌다
- 보안 검토는 구현과 다른 세션에서, 항목별로 답하게 한다
- Agent는 보안의 설계자가 아니라 성실한 점검자다