AI 퍼징 (4b 단계)
3단계 도구는 룰에 정의된 패턴을 찾습니다. 룰이 묘사하지 않은 것 — 비즈니스 로직 결함, 엣지케이스 입력 처리 — 은 정의상 걸리지 않습니다. 4b 는 그 영역을 모델이 직접 탐색해서 메웁니다. 4a 가 이미 플래그된 것을 다시 판정한다면, 4b 는 아무도 플래그하지 않은 곳을 찾습니다.
1~3단계와 4a 는 코드를 읽습니다. 4b 는 앱을 띄우고 요청을 보내 응답을 봅니다. 정적 분석이 도달하지 못하는 결함이 여기서 나오는 이유이고, 매 커밋이 아니라 주기로 도는 이유이기도 합니다.
무엇을 어떻게 하는가
| 단계 | 하는 일 |
|---|---|
| 읽기 | 앱 코드를 모델에 넘겨 엔드포인트와 파라미터를 파악합니다 |
| 생성 | 엔드포인트마다 경계값과 이상 입력을 만듭니다 |
| 실행 | 기동한 앱에 실제로 요청을 보냅니다 |
| 관찰 | 5xx 응답, 비정상 응답, 어긋나는 상태를 결함 후보로 남깁니다 |
무작위 바이트를 넣는 전통적 퍼징과 다릅니다. 모델이 엔드포인트 시그니처를 읽고 의미 있는 경계값을 만듭니다. 문자열 길이 제한 근처, 음수, 빈 값, 타입이 어긋나는 값, 경로 탐색 문자열 같은 것들입니다.
대상별 도구 조합
| 도구 조합 | 탐지 대상 | 실행 주기 |
|---|---|---|
| Claude + requests | 웹 API 엣지케이스·비정상 응답 | Push to main |
| Claude + AFL++ | 저수준 바이너리 크래시 | 주 1회 스케줄 |
| Claude + OSS-Fuzz | 오픈소스 라이브러리 파서 취약점 | 프로젝트별 설정 |
C/C++ 나 Rust 같은 저수준 코드는 모델이 입력을 만들더라도 실행 커버리지를 추적하는 쪽이 효과적입니다. 이 경우 OSS-Fuzz 연동을 권장합니다.
모델 없이 먼저 시작하는 방법
API 키가 없거나 예산 승인이 나지 않아 4b 단계를 미루고 있다면, 스키마 기반 퍼징으로 먼저 시작할 수 있습니다. 조건은 하나입니다. 애플리케이션이 OpenAPI 스키마를 노출하고 있으면 됩니다. FastAPI, NestJS, Spring 계열은 대개 기본으로 노출합니다.
스키마에는 엔드포인트마다 파라미터의 타입, 필수 여부, 길이나 범위 같은 제약이 들어 있습니다. schemathesis 같은 도구가 그것을 읽어 경계값을 만듭니다. 선언된 최댓값을 넘는 값, 빈 문자열, 타입이 어긋나는 값, 필수 필드 누락, 정의에 없는 열거형 값 같은 것들입니다. 모델도 GPU도 유료 서비스도 필요하지 않습니다.
무엇을 얻고 무엇을 못 얻는가
| 스키마 기반 | 모델 기반 | |
|---|---|---|
| 아는 것 | 타입과 제약 | 파라미터의 의미와 맥락 |
| 만드는 입력 | 선언된 경계 근처의 값 | 이름과 용도에서 유추한 값 |
| 놓치는 것 | 경로 탐색 문자열처럼 의미를 알아야 나오는 입력 | 스키마에만 적힌 세부 제약 |
| 비용 | 없음 | API 호출 비용 |
path 라는 이름의 파라미터에 ../../etc/passwd 를 넣어 볼 생각은 스키마만 봐서는 나오지
않습니다. 두 엔드포인트를 특정 순서로 호출해야 의미가 생기는 흐름도 마찬가지입니다. 비즈니스
로직 결함은 여전히 모델 쪽이 낫습니다.
그래서 스키마 기반은 4b를 대체하는 것이 아니라 4b가 메우려는 공백의 일부를 먼저 메우는 것입니다. 다만 그 일부가 작지 않습니다. TRUSCA에 붙였을 때 141개 오퍼레이션에서 약 1100개 테스트 케이스가 생성됐고 260건의 고유 실패가 나왔습니다. 응답이 스키마와 어긋나는 경우, 문서화되지 않은 상태 코드, 스키마를 위반한 요청을 받아들이는 경우가 대부분이었습니다.
붙일 때 걸린 것들
TRUSCA에 적용하며 실제로 막힌 지점입니다.
스키마 버전. FastAPI는 OpenAPI 3.1을 냅니다. schemathesis 3.x는 실험 플래그 없이는 이 스키마를 아예 읽지 않고 로딩 단계에서 종료합니다. 4.x는 정식으로 읽습니다.
도구 기본값의 데이터 전송. schemathesis 3.x에서 --report 를 인자 없이 쓰면 실행 결과가
제작사 서비스로 업로드되고, 텔레메트리 기본값이 켜져 있습니다. 사내 API의 스키마와 테스트
결과가 나갈 수 있습니다. 4.x에서는 업로드와 텔레메트리 옵션이 없어졌고 --report 는 JUnit
같은 로컬 리포트를 만드는 옵션으로 바뀌었습니다. 어느 쪽이든 도구를 붙이기 전에 기본값이
어디로 무엇을 보내는지 확인해야 합니다.
퍼징 계정의 권한 범위. 이것은 기술 결정이 아니라 정책 결정입니다. 인증 없이 돌리면 대부분 401이 돌아와 아무것도 보지 못합니다. 반대로 관리자 권한을 주면 테넌트를 넘나드는 삭제 경로까지 요청을 보냅니다. TRUSCA에서는 매 실행 새 사용자를 만들어 자기 팀의 관리자 권한만 주고, 상위 관리자 권한이 아님을 워크플로가 실행 전에 확인하도록 했습니다. 관리자 전용 경로에서 403이 돌아오는 것은 실패가 아니라 권한 경계가 동작하는 증거입니다.
비차단 설정이 만드는 사각지대
퍼징은 보통 차단 게이트로 두지 않습니다. 발견 건수가 많고 오탐도 섞이기 때문입니다. 그런데 비차단으로 만들면서 도구의 종료 코드를 삼키면, 도구가 API에 닿기도 전에 죽어도 잡은 초록이 됩니다. 결과는 "실패 0건"이고 이는 통과와 구분되지 않습니다.
TRUSCA에서 이 워크플로를 만드는 동안 세 번 그랬습니다. 리포트 파일을 쓰지 못해서, 스키마 버전을 읽지 못해서, 명령행 인자 형식이 틀려서입니다. 세 번 모두 깨끗한 요약이 나왔습니다.
대응은 발견 건수보다 먼저 커버리지 수치를 찍는 것입니다.
| API operations selected | 141 |
| Test cases generated | 1098 |
둘 중 하나라도 0이면 API에 닿지 못한 것이지 API가 깨끗한 것이 아니라고 요약에 명시합니다. 검사기가 0건을 셌다는 것과 검사 결과가 0건이라는 것은 다른 문장입니다.
실전 적용 사례 — ai-coding-best-practice
ai-coding-best-practice 저장소가 이 단계를 매주 돌립니다. 파일 두 개로 되어 있습니다.
워크플로
ai-fuzzing.yml 이 main 푸시와 매주 일요일에 실행됩니다.
on:
push:
branches: [main]
schedule:
- cron: '0 4 * * 0'
앱을 기동하고 헬스체크로 준비를 확인한 뒤 스크립트를 실행합니다. 결과 파일은 artifact 로 30일 보관합니다.
- name: Start app
run: |
python src/app.py &
sleep 5
curl -sf http://localhost:8080/health || (echo "앱 기동 실패" && exit 1)
키가 없으면 건너뜁니다. ANTHROPIC_API_KEY 가 없는 fork 나 도입 초기에도 job 이
실패하지 않습니다. 4a 워크플로와 같은 설계입니다.
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::warning::ANTHROPIC_API_KEY가 설정되지 않아 AI 퍼징을 건너뜁니다."
exit 0
fi
스크립트
scripts/ai-fuzz.py 가 세 가지를 합니다.
generate_fuzz_cases()— 앱 코드를 모델에 넘겨 엔드포인트별 케이스를 JSON 배열로 받습니다. 최소 20건을 요구하고, 인젝션·경로 탐색·비정상 입력을 탐지 목표로 지정합니다run_fuzz_cases()— 각 케이스를 실제 요청으로 보냅니다. 상태 코드가 500 미만이면 통과, 그 이상이거나 예외가 나면 실패로 기록하고 응답 앞부분을 함께 남깁니다- 결과를
fuzz-report.json으로 씁니다
도입할 때 주의할 것
차단 게이트로 쓰지 않습니다. 모델이 만든 입력이므로 오탐이 섞이고, 실패 하나로 배포를 막으면 곧 꺼지게 됩니다. 4a 와 같은 원칙입니다.
격리된 환경에서 돌립니다. 실제 요청을 보내므로 운영 환경을 대상으로 하면 안 됩니다. CI 안에서 앱을 새로 띄우고 그 인스턴스만 대상으로 합니다.
비용을 주기로 조절합니다. 매 커밋마다 20건 이상을 생성·실행하면 토큰과 시간이 함께 듭니다. 위 사례는 main 푸시와 주 1회로 제한합니다.
셀프 스터디
- 위 저장소를 fork 한 뒤
ANTHROPIC_API_KEY를 등록하고 워크플로를 수동 실행해 보세요. 키가 없을 때 job 이 어떻게 끝나는지 먼저 확인하면 도입 부담을 가늠할 수 있습니다 fuzz-report.json을 열어 실패한 케이스의description을 읽어 보세요. 모델이 무엇을 노렸는지가 적혀 있습니다- 여러분의 앱에 적용한다면
BASE_URL과 기동 명령만 바꾸면 됩니다
다음 단계
- AI 보안 코드 리뷰 — 4a, 이미 플래그된 것을 다시 판정합니다
- 에이전트와 MCP 도구 거버넌스 — 4c, AI 가 호출하는 도구를 통제합니다
- 5단계 전략 — 전체 모델에서 이 단계의 위치