Claude Code 보안 리뷰 자동화 공식 발표 이미지
이미지: claude.com/blog (Automate security reviews with Claude Code)

로그인 세션을 다루는 API 라우트를 손봤는데, 리뷰어를 기다리는 동안 PR을 올려도 되는지 확신이 안 섰습니다. 타입 체크와 린트는 CI가 알아서 걸러주지만, 입력값을 어디서 어떻게 조합해서 쿠키나 DB 쿼리로 넘기는지 같은 건 그 체크들이 보는 범위 밖이었습니다.

어떤 작업이었나

세션 토큰을 검증하는 라우트에 조건 하나를 추가하는 작업이었습니다. 쿼리 파라미터로 들어온 값을 그대로 조건문에 꽂아 쓰는 코드를 짜고 있었는데, 작성하는 도중에도 "이 값을 이대로 믹스해도 되나" 하는 불안감이 계속 들었습니다. 사람 리뷰어는 보통 로직이 맞는지부터 보고 인젝션 같은 건 뒤로 밀리는 경우가 많아서, PR을 올리기 전에 혼자서라도 한 번 걸러보고 싶었습니다.

AI를 어떻게 활용했나

터미널에서 바로 /security-review를 실행했습니다.

/security-review

이 스킬은 현재 브랜치와 base 브랜치(보통 main) 사이의 diff만 떼어내서 훑습니다. 전체 코드베이스를 다시 읽는 게 아니라 "이번에 바뀐 부분"에 집중하는 구조라, 실행 시간이 길지 않았습니다. 점검 대상은 SQL 인젝션, XSS, 인증/인가 누락, 안전하지 않은 데이터 처리, 의존성 취약점 다섯 갈래로 고정돼 있습니다.

실제로 걸린 건 제가 짠 코드 중 이런 패턴이었습니다.

// 수정 전
const userId = searchParams.get("userId");
const { rows } = await db.query(
  `SELECT * FROM sessions WHERE user_id = ${userId}`
);

리포트에는 "userId가 쿼리 문자열에 그대로 삽입되어 SQL 인젝션에 노출됩니다"라는 설명과 함께, userId=1 OR 1=1처럼 조작된 값이 들어오면 어떤 요청이 가능한지까지 구체적으로 적혀 있었습니다. 고치는 방향도 같이 제안해서, 파라미터 바인딩으로 바꿨습니다.

// 수정 후
const userId = searchParams.get("userId");
const { rows } = await db.query(
  "SELECT * FROM sessions WHERE user_id = $1",
  [userId]
);

같은 diff에서 함께 지적된 건 에러 응답에 DB 에러 메시지를 그대로 내려주던 부분이었습니다. 로직상 문제는 없지만 테이블 구조나 쿼리문이 클라이언트에 노출될 수 있다는 지적이었는데, 이건 심각도가 낮은 항목으로 따로 분류돼 있어서 인젝션 건과 우선순위가 섞이지 않았습니다.

결과와 얻은 팁

  • diff 기반이라 오래 걸리지 않는 대신, 이번에 안 건드린 코드에 있던 기존 문제는 당연히 잡아주지 않습니다. "이번에 내가 새로 들여온 위험"만 보는 도구라고 생각하는 게 맞습니다.
  • 심각도가 섞여서 나오기 때문에, 리포트를 받으면 바로 다 고치기보다 인젝션·인증 누락처럼 실제 공격 경로가 있는 항목과 정보 노출처럼 상대적으로 가벼운 항목을 먼저 구분해서 봤습니다.
  • GitHub Action으로 연결하면 PR이 올라올 때마다 자동으로 돌면서 인라인 코멘트를 달아준다고 하는데, 이번엔 로컬에서 커밋 전에 먼저 돌려보는 용도로만 썼습니다. 리뷰어가 보기 전에 뻔한 인젝션 패턴 하나를 미리 걸러낸 것만으로도, PR에서 "이 부분 확인해주세요" 코멘트를 하나 덜 받았습니다.