이 글은 oh-my-secuaudit을 만들어가는 과정을 기록한 시리즈의 세 번째 글이다.
oh-my-secuaudit 시리즈
- Security Testing as Code — 진단을 코드로 구조화하는 실험
- 취약점을 잘 찾는 사람보다, 구조를 만드는 사람이 남는다
- 228개 엔드포인트를 5개 클러스터로 줄인 이야기 ← 현재 글
이전 글에서 감각이 구조로 바뀌는 과정, 그리고 AI가 그 구조 안에서 가장 잘 동작한다는 이야기를 했다.
이번에는 그 구조의 실체를 보여주려 한다.
이론이 아니다. 슬라이드가 아니다. 실제로 만들어서 실제 코드베이스에 돌린 결과다.
1. AI가 남긴 문제
중규모 이커머스 플랫폼에 AI 기반 보안 진단을 돌린 뒤, 결과가 나왔다. 너무 많이.
시스템은 12개 모듈, 228개 엔드포인트, 73개 컨트롤러로 구성돼 있었다. 도메인별로 분리한 AI 에이전트들이 수백 건의 결과를 쏟아냈다. 정확한 것도 있었고, 과장된 것도 있었고, 같은 문제를 세 가지 이름으로 보고한 것도 있었다.
탐지 단계는 해결됐다. 남은 건 해석이었다.
228개 엔드포인트 중 실제로 수동 리뷰가 필요한 건 어디인가. 하나에서 취약점을 찾으면 같은 패턴을 공유하는 형제는 몇 개인가. 코드가 바뀌면 어떤 결과를 다시 확인해야 하는가.
이건 스캔을 더 돌린다고 답이 나오는 문제가 아니다. 구조가 필요하다.
2. 코드 유사성이 아니라, 보안 의미로 클러스터링한다
내가 도달한 방법은 클러스터링이었다. 하지만 흔히 생각하는 그 클러스터링은 아니다.
코드 유사성 클러스터링은 왜 안 되는가
코드 유사성 기반 클러스터링은 비슷하게 생긴 파일을 묶는다. 리팩토링에는 유용하다. 보안에는 쓸모없다.
구체적인 예를 들겠다. 주문 승인 컨트롤러와 쿠폰 발송 컨트롤러가 있다고 하자. 둘 다 @RestController이고, 같은 BaseController를 상속하고, @PostMapping으로 요청을 받아 서비스 레이어를 호출한다. 코드 구조는 90% 이상 동일하다.
코드 유사성 기반 클러스터링은 이 둘을 같은 클러스터에 넣는다.
하지만 주문 승인 컨트롤러는 WebConfig에 인증 인터셉터가 등록돼 있고, 쿠폰 발송 컨트롤러에는 없다. 하나는 인증된 사용자만 접근 가능하고, 다른 하나는 누구나 호출할 수 있다. 코드가 같다는 이유로 이 둘에 같은 리뷰 전략을 적용하면, 인증 부재를 놓친다.
코드가 비슷하다는 것과 보안 상태가 같다는 것은 전혀 다른 이야기다.
보안 의미를 결정하는 다섯 가지 요소
그래서 코드 모양이 아니라, 데이터가 시스템을 관통하는 흐름에 집중했다. 보안적으로 의미 있는 흐름은 다섯 가지 요소로 분해된다.
| 요소 | 의미 | 예시 |
|---|---|---|
| Source | 데이터 진입점 | HTTP 파라미터, 요청 본문, 외부 API 응답 |
| Transformation | 가공·처리 로직 | 문자열 조작, 파싱, 객체 변환 |
| Validation / Sanitization | 필터링·인코딩 여부 | HTML 이스케이프, SQL 파라미터 바인딩, allowlist 적용 |
| Sink | 최종 출력 지점 | DB 쿼리 실행, HTML 응답, 로그 출력, 역직렬화 |
| Context | 보안 컨텍스트 | 인증 상태, 데이터 민감도, 네트워크 경계 |
같은 Source에서 출발해도 Sink가 다르면 위협이 다르다. 같은 Sink에 도달해도 중간에 Validation이 있느냐 없느냐에 따라 취약 여부가 갈린다. 같은 코드가 Context(인증 적용 여부)에 따라 안전할 수도, 치명적일 수도 있다.
(Endpoint, Sink)가 최소 단위인 이유
이 다섯 요소를 기반으로 클러스터링 단위를 정의했다.
(Endpoint, Sink) — source에서 sink까지의 dataflow 경로.
왜 엔드포인트만으로 안 되는가? 하나의 엔드포인트가 DB에도 쓰고, 로그에도 쓰고, 외부 API도 호출할 수 있다. 각각 다른 보안 리뷰가 필요하다.
왜 sink만으로 안 되는가? 같은 log.info() sink라도 인증된 내부 관리 화면에서 호출되는 것과 외부에 노출된 API에서 호출되는 것은 위험도가 다르다.
구체적으로 이렇게 된다.
| Endpoint | Sink | 분석 단위 |
|---|---|---|
| 주문 승인 API | DB 상태 변경 | 인증/인가 리뷰 (C1) |
| 주문 승인 API | log.info(주문번호) | 민감정보 로깅 리뷰 (C4) |
| 주문 수신 API | XStream.fromXML() | 역직렬화 리뷰 (C5) |
| PG 연동 모듈 | HttpsURLConnection | SSL 검증 리뷰 (C3) |
같은 “주문 승인 API"가 두 개의 다른 클러스터에 들어간다. Sink가 다르기 때문이다. 이게 (Endpoint, Sink) 조합이 최소 단위인 이유다.
핵심 원칙
클러스터는 동일 결과를 보장하지 않는다. 클러스터는 동일한 리뷰 전략 적용 가능성을 제공한다.
클러스터를 신뢰하는 게 아니라 검증하면서 활용한다. 하지만 대표 샘플 5개를 검증하는 것은 228개 엔드포인트를 각각 리뷰하는 것과는 비교할 수 없을 만큼 효율적이다.
3. 5개 클러스터가 나타났다
228개 엔드포인트를 (Endpoint, Sink) 기준으로 재구성한 뒤, 보안 의미가 유사한 경로끼리 묶었다. 결과는 5개 클러스터였다. 가장 큰 클러스터(C1)는 대표 샘플 5개로 시작하고, 작은 클러스터(C2)는 5건 전수 리뷰. 228개를 각각 보는 대신, 초기 검증 대상이 약 20개로 줄어든다.
각 클러스터가 어떤 보안 문제이고, 왜 클러스터링 대상인지 설명한다.
C1: 엔드포인트 레벨 인증 부재
플랫폼에는 모든 API 모듈 앞에 위치한다고 가정되는 인증 게이트웨이(AuthGW)가 있었다. 하지만 실제 코드에는 자체 인증 메커니즘이 없는 컨트롤러가 다수였다 — @PreAuthorize 없음, @Secured 없음, WebConfig에 인터셉터 미등록. AuthGW라는 네트워크 레벨 통제에만 의존하고 있었고, 그 가정이 깨지면 쿠폰 승인, 주문 취소, 환불 처리 같은 고가치 API가 무방비 상태가 된다.
왜 클러스터링 대상인가? 공통 인증 모듈이 존재해도, 개별 엔드포인트에서 실제로 호출되는지가 핵심이다. 228개 엔드포인트 각각을 확인하려면 엔드포인트별 검사가 필요하고, 클러스터링 없이는 리뷰 비용이 폭발한다.
초기 추정: 10개 이상 모듈에 걸쳐 ~120개 엔드포인트.
C2: 하드코딩된 공유 시크릿
일부 컨트롤러는 인증을 아예 다른 방식으로 처리하고 있었다. 요청 본문에서 특정 필드를 꺼내서, 코드에 하드코딩된 문자열과 String.equals()로 비교하는 것이다. 일치하면 작업이 진행되고, 불일치하면 return true로 그냥 통과시키는 — 차단조차 안 하는 — 구조였다.
// 실제 패턴 (익명화)
if (request.getPromisedValue().equals("serviceManualProcessing1111")) {
// 수기 처리 실행
}
외부 파트너별로 다른 5개 컨트롤러에서 동일한 패턴이 반복됐다. 비밀번호 리터럴만 파트너명에 맞게 바뀌었을 뿐, 구조는 완전히 같았다.
왜 클러스터링 대상인가? 패턴이 완전히 동일하므로 대표 1개만 검증하면 나머지 4개가 자동으로 커버된다 (4배 절감). 그리고 정적 패턴 매칭으로 100% 검출이 가능하다 — Semgrep 룰 하나로 끝난다.
Semgrep 결과: 2개 모듈에서 10건 매칭.
C3: SSL/호스트명 검증 우회
이 플랫폼은 여러 외부 PG사(결제 대행사)와 HTTPS로 통신했다. 문제는 그 HTTPS 연결을 설정하는 코드에 있었다.
Java에서 HTTPS 연결을 만들 때, TrustManager는 서버 인증서를 검증하고, HostnameVerifier는 인증서의 호스트명이 실제 접속 대상과 일치하는지 확인한다. 이 두 검증을 모두 무력화하는 코드가 반복적으로 나타났다.
// TrustManager — 모든 인증서를 무조건 신뢰
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// 비어 있음 — 검증 없음
}
// HostnameVerifier — 모든 호스트명을 무조건 허용
public boolean verify(String hostname, SSLSession session) {
return true;
}
이렇게 되면 HTTPS 연결이 있어도 중간자 공격(MITM)에 무방비 상태가 된다. 공격자가 네트워크 중간에서 통신을 가로채고 위조된 응답을 돌려보낼 수 있다. 결제 응답이 위조되면 금전적 피해로 직결된다.
왜 클러스터링 대상인가? PG사마다 별도의 연동 클래스가 있었지만, SSL 우회 패턴은 동일했다. 하나의 연동 클래스에서 검증하면 나머지에도 같은 결론이 적용된다.
초기 추정: 3개 이상 모듈에 10건 이상. Semgrep 결과: 4개 모듈에서 56건 — 초기 추정 대비 5배. PG 연동 클래스가 예상보다 훨씬 많았다.
C4: 민감 데이터 로깅
민감 데이터 로깅은 반복 패턴 + 전역 설정 영향 + 인증정보 노출이라는 세 층으로 나타났다.
C4-a: 식별자 직접 로깅. 컨트롤러 메서드 진입 시점에 쿠폰 번호, 주문 번호, 회사 ID를 log.info()로 직접 출력. 개별 컨트롤러마다 반복.
C4-b: 전역 요청/응답 본문 로깅. Spring의 RequestBodyAdviceAdapter나 AOP @Around 어드바이스로 모든 요청·응답 본문을 통째로 캡처. 해당 모듈의 모든 엔드포인트가 자동으로 영향을 받는다 — 개발자가 개별 컨트롤러에서 로깅 코드를 작성하지 않았어도.
C4-c: 인증 정보 로깅. 파트너사 API 호출 시 Authorization 헤더의 Basic 인증키를 로그에 출력.
왜 클러스터링 대상인가? C4-a는 반복 패턴이라 대표 샘플로 커버. C4-b는 AOP 클래스 1개 리뷰로 모듈 전체 커버. C4-c는 개별 케이스지만 같은 리뷰 전략을 적용할 수 있다.
초기 추정: 4개 이상 모듈. Semgrep 결과: 9개 모듈에서 90건 — 초기 추정 대비 10배. AOP 전역 로깅의 영향 범위를 과소평가했다.
C5: 안전하지 않은 XML 역직렬화 (XXE / allowlist 미적용)
이 플랫폼은 외부 파트너 시스템과 XML 형식으로 데이터를 주고받았다. XML을 Java 객체로 변환하는 데 XStream 라이브러리를 사용했는데, 두 가지 보안 설정이 빠져 있었다.
첫째, allowlist 미적용. XStream.fromXML()은 XML 안에 지정된 클래스를 그대로 인스턴스화한다. setupDefaultSecurity()나 allowTypes()로 허용할 클래스를 제한해야 하는데, 이 설정이 없었다. 공격자가 XML에 위험한 클래스(예: Runtime.exec()를 호출하는 가젯 체인)를 넣으면 서버에서 임의 명령이 실행된다.
둘째, XXE 방어 미적용. DocumentBuilderFactory.parse()에서 외부 엔티티(External Entity) 로딩을 비활성화하지 않았다. 공격자가 XML에 외부 엔티티 참조를 넣으면, 서버가 내부 파일을 읽거나 내부 네트워크에 요청을 보낼 수 있다.
더 심각한 문제는, 취약한 XStreamUtil.java 유틸리티 클래스가 여러 모듈에 복사-붙여넣기돼 있었다는 것이다. 한 곳에서 고쳐도 다른 곳에는 여전히 취약한 복사본이 남는다.
왜 클러스터링 대상인가? 복사된 유틸리티 클래스들이 동일한 패턴을 공유하므로, 원본 1개만 검증하고 나머지는 diff를 비교하면 된다. 패턴 기반 Semgrep 룰로 모든 사용처를 자동 검출할 수 있다.
Semgrep 결과: API 레이어에서 2건 매칭 — 하지만 백엔드 모듈은 아직 스캔 미실시. 실제 규모는 더 클 가능성이 높다.
4. 클러스터를 공격 시나리오에 매핑하다
5개 클러스터가 정의됐다. 그다음 단계는 뭘까?
각 클러스터를 개별적으로 리뷰하는 것도 가치가 있다. 하지만 실제 공격은 하나의 취약점만으로 성립하지 않는 경우가 많다. 공격자는 여러 약점을 조합한다. 인증이 빠진 엔드포인트(C1)와 역직렬화 취약점(C5)이 각각 다른 클러스터에 있어도, 같은 엔드포인트에서 만나면 공격 체인이 된다.
이걸 체계적으로 확인하기 위해, 클러스터와 공격 시나리오를 교차시키는 매트릭스를 만들었다. 이 플랫폼에서 식별한 주요 공격 시나리오(쿠폰 승인/취소 변조, 주문 탈취, 수기처리 오남용, 파트너 연동 위변조 등)를 행으로, 5개 클러스터를 열로 놓고, 각 시나리오에 어떤 클러스터가 관여하는지 표시했다.
| 공격 시나리오 | C1 | C2 | C3 | C4 | C5 |
|---|---|---|---|---|---|
| 고가치 상태변경 (승인/취소) | ● | ||||
| 주문 수신/재발송/환불 | ● | ● | |||
| 수기처리 오남용 | ● | ||||
| 계정 탈취 기반 운영자 악용 | ● | ● | |||
| 파트너 연동 SSL 무력화 | ● | ● | |||
| 파트너 인증정보 로그 노출 | ● | ||||
| AuthGW 우회 | ● | ||||
| POS/실시간 승인 직접 호출 | ● | ● |
이 매트릭스에서 두 가지가 바로 보인다.
첫째, C1이 시나리오의 절반 이상에 걸쳐 있다. 인증/인가 부재는 단독으로도 위험하지만, 다른 클러스터의 취약점을 증폭시킨다. C1의 대표 샘플 리뷰가 가장 큰 리스크 절감 효과를 가진다.
둘째, 하나의 시나리오에 두 개 이상의 클러스터가 동시에 ●로 표시된 행이 있다. “주문 수신/재발송/환불"은 C1과 C5가 동시에 걸려 있고, “POS/실시간 승인 직접 호출"도 마찬가지다. 이 교차점이 바로 복합 공격 체인이 성립할 수 있는 지점이다.
각 클러스터를 독립적으로 리뷰하면 이 교차점을 놓친다. 하지만 매트릭스를 한 번 만들어놓으면, “이 시나리오에는 어떤 클러스터들이 동시에 관여하는가?“가 한눈에 보인다.
실제로 가장 위험한 발견도 이 매트릭스에서 나왔다.
5. 교차점에서 발견된 RCE 체인
매트릭스에서 C1과 C5가 동시에 걸린 행을 추적했다. “주문 수신” 시나리오였다.
외부 파트너로부터 B2C 주문을 수신하는 하나의 엔드포인트에 다음 두 가지가 동시에 존재했다.
- 인증 없음 (C1: 인터셉터 없음, AuthGW 가정에만 의존)
- allowlist 없는 XStream 역직렬화 (C5: 신뢰할 수 없는 XML에 대해
fromXML()호출)
각각을 따로 보면 이렇다.
C1 단독 — 인증이 없다면? 공격자가 그 엔드포인트를 호출할 수 있다. 하지만 할 수 있는 건 정상적인 주문 요청뿐이다. XML을 보내면 시스템이 정해진 주문 처리 로직을 실행한다. 불편하지만 즉각적인 시스템 장악은 아니다. 보안 진단 보고서에서는 보통 “Medium” 정도로 분류된다.
C5 단독 — 역직렬화 취약점이 있다면?
XStream.fromXML()은 XML을 Java 객체로 변환한다. allowlist가 없으면 공격자가 XML 안에 임의의 Java 클래스를 지정할 수 있고, 서버는 그 클래스를 실제로 인스턴스화한다. 이걸 이용하면 서버에서 운영체제 명령을 실행할 수 있다 — 이른바 원격 코드 실행(RCE).
하지만 인증이 있다면? 공격자는 먼저 유효한 자격증명을 획득해야 하고, 그 시점에서 공격 난이도가 크게 올라간다. 보안 진단 보고서에서는 “High이지만 선행 조건 있음"으로 분류될 수 있다.
C1 x C5 — 합쳐지면? 인증이 없으니 누구나 그 엔드포인트에 도달할 수 있다. 역직렬화에 allowlist가 없으니 임의의 클래스를 인스턴스화할 수 있다. 두 조건이 합쳐지면:
- 공격자가 네트워크에서 엔드포인트에 접근한다 (인증 없음 → 가능)
- 조작된 XML을 전송한다 (정상 요청과 동일한 형식)
XStream.fromXML()이 XML을 파싱하면서 공격자가 지정한 Java 클래스를 인스턴스화한다- 해당 클래스의 생성자 또는 메서드가 실행되면서 운영체제 명령이 실행된다
선행 조건 없는 원격 코드 실행. 보안에서 가장 심각한 등급이다.
C1을 개별적으로 리뷰할 때는 “인증 게이트웨이가 앞에 있으니 위험도 낮음"으로 넘어갈 수 있다. C5를 개별적으로 리뷰할 때는 “내부 통신이라 접근 자체가 제한됨"으로 넘어갈 수 있다. 각각은 합리적인 판단이다. 하지만 둘을 교차시키면 두 판단의 전제가 동시에 무너진다.
이것이 클러스터링이 효율성을 넘어 중요한 이유다. 클러스터를 공격 시나리오에 매핑하면, 개별 결과가 놓치는 상호작용 효과가 보인다. 각 셀을 독립적으로 확인하는 취약점 매트릭스로는 절대 이걸 찾을 수 없다. 진짜 위험은 교차점에 산다.
6. 클러스터를 검증하는 과정
클러스터링에 대한 흔한 반론은 이것이다: “클러스터가 맞는지 어떻게 알아?”
모른다. 처음에는.
클러스터는 가설이다. 대표 샘플을 리뷰해서 가설을 검증하는 과정이 부트스트래핑이다. 이 과정에서 클러스터의 유효성을 확인하기도 하고, 예상하지 못한 취약점을 발견하기도 한다.
리뷰 중 발견: 1줄짜리 버그
C1 대표 샘플 리뷰를 시작하면서, 나는 단순히 “인증 인터셉터가 있는가 없는가"만 확인한 게 아니었다. 클러스터링의 Feature 테이블에서 Context 항목 — 인증 상태, AuthGW 경유 가정 — 을 검증하려면 실제 인증 메커니즘의 동작을 읽어야 했다.
AuthGW 모듈에는 POS API 엔드포인트를 위한 IP 허용 목록 검사가 있었다. 메서드는 boolean 값 permIpCheck — 요청 IP가 허용 목록에 있는지 — 를 계산한 뒤, 맨 마지막에:
return true; // 올바른 코드: return permIpCheck;
1줄. POS 엔드포인트 5개에 대한 IP 허용 목록 전체가 무력화돼 있었다.
정교한 취약점이 아니다. 모든 코드 리뷰를 살아남은 오타다. 엔드포인트 단위 스캔에서는 보이지 않는다 — 엔드포인트 레벨 체크리스트가 인증 유틸리티 코드까지 읽으라고 지시하지 않기 때문이다.
클러스터링이 나를 거기로 이끌었다. C1 대표 샘플 리뷰는 인증 메커니즘의 존재 여부가 아니라, 실제 동작을 검토하도록 요구했다. 이건 클러스터 정의 단계에서 계획한 리뷰가 아니라, 부트스트래핑 과정의 부산물이었다.
단계적 검증
그래서 단계적 검증 방식을 채택했다. 처음부터 클러스터를 신뢰하는 게 아니라, 수동 리뷰를 통해 점진적으로 신뢰도를 확보한다.
| 단계 | 조건 | 샘플링 | 실제 적용 예시 |
|---|---|---|---|
| 1단계 (초기) | 클러스터 신뢰도 미형성 | 50% 이상 수동 리뷰 | C2(5개): 전수 리뷰. C1(120+개): 대표 5개 우선 |
| 2단계 (안정화) | 클러스터 내 일치율 >= 80% | 30%로 축소 | C3: 5개 리뷰 후 전부 동일 패턴 → 30%로 축소 |
| 3단계 (운영) | 누락률 < 5% 연속 2회 | 대표 샘플만 | C2: 전수 확인 완료 → 이후 대표 1개만 |
10개 미만의 작은 클러스터(C2의 5건)는 처음부터 전수 수동 리뷰를 한다 — 어차피 비용이 작다. C1처럼 100개 이상인 큰 클러스터는 위험도 최상위 대표 샘플 5개부터 시작하고, 결과에 따라 확장한다.
핵심 지표: 클러스터 내부 일관성
핵심 지표는 클러스터 내부 일관성이다. 클러스터 안에서 같은 취약점 판정을 공유하는 샘플이 몇 퍼센트인가?
예를 들어 C3에서 5개 샘플을 리뷰했다고 하자. 5개 모두 HostnameVerifier가 return true를 반환하는 동일 패턴이면 일관성은 100%다. 이 경우 2단계로 넘어갈 수 있다.
반면 5개 중 2개는 실제로 적절한 TLS 검증이 돼 있다면? 일관성은 60%다. 이건 클러스터 정의가 잘못됐다는 신호다. 이 경우 클러스터를 분할해야 한다 — 예를 들어 “C3-a: 완전 우회"와 “C3-b: 부분 완화 있음"으로 나누고, 각각에 다른 리뷰 전략을 적용한다.
리뷰어가 2명 이상일 때는 Cohen’s Kappa (리뷰어 간 판정 일치율)도 측정한다. Kappa가 낮으면 클러스터 품질 문제가 아니라 리뷰 기준이 불일치한다는 신호다. 이 구분이 중요하다 — 클러스터를 분할할 게 아니라 리뷰 기준을 정렬해야 한다.
클러스터링이 실패하는 경우
모든 코드가 클러스터링에 적합한 건 아니다. 다음 조건에서는 정적 분석으로 dataflow를 추적할 수 없어서 클러스터링이 의미를 잃는다.
| 조건 | 왜 실패하는가 | 예시 |
|---|---|---|
| Reflection / dynamic dispatch | 실제 호출 대상이 런타임에 결정돼 정적 분석으로 추적 불가 | Class.forName(className).newInstance() |
| AOP / Proxy 기반 흐름 | 보안 처리 로직이 런타임 프록시를 통해 주입되므로 코드에 보이지 않음 | Spring AOP @Around 어드바이스 |
| 프레임워크 내부 hidden flow | dataflow가 프레임워크 내부에서 끊김 | Spring Security 필터 체인 내부 |
| Runtime config 의존 sanitizer | 동일 코드도 설정 파일에 따라 다르게 동작 | 환경별로 다른 암호화 설정 |
| 템플릿 엔진 내부 처리 | 템플릿 엔진의 자동 이스케이프 여부를 정적으로 판단할 수 없음 | Thymeleaf th:utext vs th:text |
이 경우에 해당하는 코드는 클러스터에서 제외하고 별도 목록으로 관리한다. 리뷰 우선순위는: 외부 입력과 직접 연결된 경로 → 인증 우회 가능성이 있는 경로 → 나머지 순서. 개별 수동 리뷰를 진행하되, 같은 실패 조건에 해당하는 코드가 반복될 경우 체크리스트를 만들어 일관성을 확보한다.
실제로 이 프로젝트에서도 C4의 AOP 기반 전역 로깅(C4-b)이 이 경우에 해당했다. @Around 어드바이스의 pointcut 선언으로 적용 대상이 런타임에 결정되기 때문에, 정적 dataflow만으로는 영향 받는 엔드포인트를 완전히 열거할 수 없었다. AOP 적용 대상을 별도로 전수 점검하는 fallback 절차를 적용했다.
재검증 트리거
클러스터가 3단계(운영)에 도달했다고 해서 영원히 유효한 건 아니다. 다음 변경이 발생하면 해당 클러스터를 1단계로 되돌린다.
- 코드베이스 주요 변경: 모듈 추가/제거, 프레임워크 버전 교체
- 인증/보안 설정 변경:
WebConfig,SecurityConfig,web.xml수정 PR - 신규 파트너 연동 추가: C3(SSL), C5(XML 파싱) 영향
- 신규 로깅 AOP/인터셉터 추가: C4 영향
- 누락 취약점 발견: 해당 클러스터의 가정이 틀렸다는 직접 증거
7. fortify_ml에서 sec-cluster까지
이 클러스터링 접근법에는 전사(前史)가 있다.
fortify_ml: 출발점
몇 년 전 나는 fortify_ml이라는 도구를 만들었다. Fortify SAST 스캔 결과를 입력으로 받아서, dataflow 경로를 자동으로 클러스터링하는 도구다.
동작 원리는 이렇다. Fortify는 소스 코드를 스캔한 뒤 취약점 후보를 TraceNode — source에서 sink까지의 경로 — 형태로 출력한다. fortify_ml은 이 TraceNode를 추출하고, 각 경로를 TF-IDF 벡터로 변환한 뒤, K-means 알고리즘으로 유사한 경로끼리 묶는다.
결과적으로 Fortify가 보고한 수백 건의 결과가 K개의 클러스터로 압축된다. 리뷰어는 클러스터당 대표 샘플만 확인하면 되므로, 리뷰 시간이 대폭 줄어든다.
fortify_ml은 동작했다. 하지만 한계가 명확했다.
fortify_ml의 한계와 sec-cluster로의 진화
fortify_ml에는 세 가지 한계가 있었다. Fortify 라이선스 종속 (없으면 못 씀), 워크플로 부재 (클러스터링만 하고 검증·기록·재검증은 매번 수동 설계), C1 커버 불가 (Fortify가 “인증 적용 여부"를 탐지하지 않으므로 가장 중요한 클러스터가 빠짐).
이 한계를 해결하기 위해 v4 전략에서는 dataflow 추출 도구의 우선순위를 정의했다.
| 우선순위 | 도구 | 특성 |
|---|---|---|
| 1순위 | Fortify + fortify_ml | 가장 정확하지만 상용 라이선스 필요 |
| 2순위 | Semgrep (taint tracking) | 무료, 오픈소스, 커스터마이징 용이 |
| 3순위 | CodeQL / Joern | 심층 분석 가능하지만 빌드 환경 구성 필요 |
이번 프로젝트에서는 Fortify가 없었으므로 Semgrep을 주력으로, C1은 별도 셸 스크립트(auth_enum.sh)로 처리했다.
sec-cluster: 범용화된 워크플로
fortify_ml이 “결과를 묶어주는 도구"였다면, sec-cluster는 도구가 아니라 워크플로다. oh-my-secuaudit 툴킷의 스킬로, 도구 종속 없이 소스 코드를 직접 입력으로 받는다.
| 관점 | fortify_ml | sec-cluster |
|---|---|---|
| 입력 | Fortify 스캔 결과 | 소스 코드 (도구 무관) |
| 범위 | 클러스터링만 | 6단계 전체 워크플로 |
| 산출물 | 클러스터 그룹 | CLUSTERS.md, 리뷰 체크리스트, Semgrep 룰 |
6단계 워크플로:
- Scope — 클러스터링 대상과 정적 패턴 매칭 대상 구분
- Semgrep sweep — 템플릿 룰 4개를 프로젝트에 맞게 적응,
sweep.sh로 전 모듈 일괄 실행 - Auth enumeration —
auth_enum.sh로 컨트롤러별 인증 커버리지 매핑 (C1 전용) - Cluster definition — (Endpoint, Sink) 그룹 정의, Feature 테이블 문서화
- Bootstrapping review — 단계적 샘플링, 판정 기록, 일관성 측정
- Output —
CLUSTERS.md,REVIEW_CHECKLIST.md,SUMMARY.md생성
이 프로젝트에서 Semgrep sweep을 돌린 결과, 초기 감각으로 추정한 것과 실제 탐지 범위 사이에 큰 격차가 있었다. C3(SSL 우회)는 추정 대비 5배, C4(민감 데이터 로깅)는 10배로 확대됐다. 감각은 클러스터 가설을 제공하지만, 도구가 경계를 검증한다. 자동화된 탐지 없이는 클러스터 범위를 한 자릿수 배로 과소평가했을 것이다.
프로세스를 아티팩트로 인코딩한 것이다. 다음 사람이 — 혹은 다음 AI 에이전트가 — 처음부터 시작하지 않아도 되도록.
8. 결론
이 글은 이전에 했던 주장의 직접적인 연속이다.
- 감각은 어디를 봐야 하는지 알려준다.
- 구조는 발견한 것을 어떻게 정리할지 알려준다.
- AI는 탐지를 확장한다.
- 클러스터링은 결과를 행동 가능하게 만든다.
보안 업계는 “더 많이 찾기” 문제를 대부분 해결했다. 남은 건 “이해하고 행동하기” 문제다. 클러스터링은 그 문제에 대한 구체적인 답 하나다 — 유일한 답은 아니지만, 검증된 답이다.
sec-cluster 스킬, 전략 문서, Semgrep 템플릿 모두 오픈소스다. 코드 레벨 보안 진단을 대규모로 하고 있다면, 내가 수개월의 반복 끝에 만든 것들이 시간을 아껴주길 바란다.
탐지는 이제 충분히 많이 할 수 있다. 병목은 해석과 우선순위화다. 개별 셀에서 위험을 찾지 말고, 교차점을 봐야 한다.