새로운 코드 사냥하기: 다른 누구보다 먼저 빠르게 배포되는 AI 인프라의 버그를 찾는 법
요약
AI/ML 인프라의 빠른 배포 주기를 활용하여 버그를 찾는 전략을 소개합니다. 전체 코드베이스 대신 릴리스 간의 차이점(diff)에 집중하여 새로운 공격 표면을 식별하는 방법론을 다룹니다.
핵심 포인트
- AI 인프라의 빠른 업데이트 주기를 활용한 코드 최신성 공략
- 전체 리포지토리가 아닌 릴리스 간의 차이점(diff) 분석
- 새로운 엔드포인트, 핸들러, 템플릿 등 입력값 접점 식별
- 변경 로그를 통한 개발자의 의도 및 취약점 추론
대부분의 버그 바운티 헌터(bug bounty hunters)들은 시작하기도 전에 패배합니다. 모두가 똑같은 곳에서 낚시를 하기 때문입니다. 그들은 인기 있는 프로젝트를 클론(clone)하고, 스캐너를 돌린 뒤, 지난 3년 동안 모두가 검색해 온 것과 똑같은 패턴을 grep으로 찾아냅니다. 당신이 도착했을 때쯤이면, 가치 있는 모든 정적 분석 결과(static finding)는 이미 수정되었거나, 보고되었거나, 누군가의 초안(draft) 상태에 있습니다. 코드베이스가 이미 짓밟힌 상태인 것입니다.
그래서 저는 오래된 코드를 사냥하는 것을 그만두었습니다. 저는 지난주에는 존재하지 않았던 코드를 사냥합니다.
이것은 huntr와 같은 플랫폼에서 AI/ML 인프라를 버그 사냥할 때 제가 사용하는 가장 유용한 관점입니다: 바로 코드의 최신성 (recency of code) 입니다. AI 인프라 — RAG 엔진, 에이전트 프레임워크 (agent frameworks), 벡터 파이프라인 (vector pipelines), 모델 서버 (model servers) — 는 한 달에도 여러 번 릴리스될 정도로 말도 안 되게 빠르게 배포됩니다. 모든 릴리스는 새로운 HTTP 라우트(routes), 새로운 파일 파서 (file parsers), 새로운 외부 커넥터 (external connectors), 새로운 템플릿 렌더링 (template rendering)을 추가합니다. 즉, 공격 표면 (attack surface)으로 위협 모델링 (threat-modeled) 된 적이 없는 시스템에 신뢰할 수 없는 입력값을 주입할 수 있는 새로운 방법들이 추가되는 것입니다. 그 코드는 정확히 단 한 집단, 즉 서둘러 코드를 작성한 유지 관리자 (maintainers)들에 의해서만 리뷰되었습니다. 이전의 헌터들은 스윕 (sweep) 기간 동안 해당 코드가 존재하지 않았기 때문에 결코 건드리지 못했습니다.
그 간극이 바로 기회 전체입니다.
전체 리포지토리가 아닌, 릴리스 간의 차이점(Diff)을 보세요
저는 프로젝트 전체를 읽지 않습니다. 저는 델타 (delta, 차이점) 를 읽습니다. 워크플로우는 의도적으로 지루하게 구성합니다:
# 관심 있는 두 경계 지점을 고정합니다
git fetch --tags
git log --oneline v1.2.0..v1.3.0 # 마지막 컷 이후에 반영된 내용
...
저는 신뢰할 수 없는 입력값으로부터 도달할 수 없는 모든 것은 버립니다. 리팩토링 (Refactors), 테스트 (tests), 독스트링 (doc strings), 아무 작업도 하지 않는 의존성 업데이트 (no-op dependency bumps) — 모두 삭제합니다. 살아남는 것은 새로운 싱크 (sinks)와 새로운 소스 (sources) 의 짧은 목록입니다: 새로운 엔드포인트 (endpoint), 새로운 업로드 핸들러 (upload handler), 새로운 "이 URL을 가져와 줘" 기능, 사용자 데이터를 보간(interpolate)하는 새로운 프롬프트 템플릿 (prompt template), 새로운 내보내기/가져오기 (export/import) 경로 등이 그것입니다.
diff(변경 사항)를 읽는 것은 의도(intent)를 재구성하는 방법이기도 합니다. "원격 소스로부터 지식을 가져오는 기능 추가"라고 적힌 릴리스 노트(release note)는 서버 측 요청 위조 (SSRF, Server-Side Request Forgery)를 가리키는 번쩍이는 화살표와 같습니다. "응답을 위한 사용자 정의 가능한 템플릿 추가"는 템플릿 주입 (template injection)을 시사합니다. 변경 로그 (changelog)는 개발자들이 어디에 권한을 추가했는지 알려줍니다. 빠르게 추가된 권한은 부주의하게 추가되었을 가능성이 높습니다.
신뢰할 수 없는 입력 (untrusted-input) 표면을 순서대로 분류하기
새로운 코드를 확보하면, 저는 고정된 체크리스트를 바탕으로 모든 새로운 엔트리 포인트 (entry point)를 분류 (triage)합니다. 저는 영리해지려는 것이 아니라, _완전(complete)_해지려고 노력하는 것입니다. 왜냐하면 완전함이야말로 대중을 앞설 수 있는 방법이기 때문입니다.
- SSRF — 사용자가 제공한 URL/호스트를 받아 서버가 이를 가져오게 하는 모든 것: "이 링크에서 가져오기", "이 원격 데이터셋 로드하기", 웹훅 (webhook) 콜백, 이미지 페처 (image fetcher). 허용 목록 (allowlist)이 없거나 내부 IP 대역에 대한 차단이 없는 상태에서 입력값으로 구축된 요청을 찾으세요.
- Authz / IDOR — 객체 ID를 받지만 _소유권 (ownership)_을 확인하지 않고 인증 (authentication)만 확인하는 새로운 엔드포인트 (endpoint). 빠른 속도를 중시하는 팀은 라우트 (route)와
@login_required데코레이터 (decorator)를 추가하고, "이 사용자가 리소스 N의 소유자인가"라는 단계를 잊어버리곤 합니다. - 주입 (Injection - SQL / NoSQL / command) — 입력을 연결 (concatenate)하는 새로운 쿼리 빌더 (query builder), 문서를 변환하거나 모델 바이너리 (model binary)를 호출하기 위한 새로운 셸 아웃 (shell-out).
- SSTI — 사용자가 제어하는 문자열이 입력되는 템플릿 엔진 (template engine). "프롬프트 템플릿 (prompt template)"과 "보고서 템플릿 (report template)"이 무해해 보이지만 서버 측에서 렌더링되는 LLM 툴링 (LLM tooling) 환경에서 흔히 발생합니다.
- 경로 탐색 (Path traversal) — 기본 디렉토리와 사용자가 제공한 이름을 결합하는 새로운 파일 읽기/쓰기/내보내기 기능. 누군가 "파일 다운로드" 기능을 추가한 곳이라면 어디든 고전적인
../../etc/...공격이 존재합니다. - 안전하지 않은 역직렬화 (Insecure deserialization) — 사용자가 영향을 미칠 수 있는 경로로부터 pickle, YAML 또는 모델 아티팩트 (model artifact)를 로드하는 새로운 코드. 모델 파일과 설정 파일이 당연하게 역직렬화되는 머신러닝 (ML) 분야는 이것으로 가득 차 있습니다.
특정한 버그가 아니라, 제가 찾는 일반적인 형태는 다음과 같습니다:
# 이번 릴리스의 새로운 기능 — 사용자 지정 리소스 가져오기
@router.post("/v2/resource/import")
def import_resource(source: str): # SOURCE: 신뢰할 수 없음
...
단 다섯 줄의 새로운 코드 안에 세 가지 잠재적 버그 클래스(bug classes)가 존재합니다. 이것이 팀이 빠르게 움직일 때 나타나는 새로운 디프(diff)의 모습입니다.
감사를 확산시키고, 기본적으로 반박하라
이 지점이 바로 AI가 제값을 하는 곳이자, 대부분의 사람들이 AI를 잘못 사용하는 곳입니다. 하나의 모델을 디프에 지정하고 "취약점(vulns)이 있어?"라고 묻는 것은 확신에 찬 쓰레기 더미를 얻는 것과 같습니다. 버그 바운티(bounty) 플랫폼에서 오탐(False positives)은 공짜가 아닙니다. 가짜 보고서의 흐름은 당신의 평판을 떨어뜨리며, 저품질 제출에 페널티를 부여하는 플랫폼에서는 계정을 잃을 수도 있습니다. 계정은 곧 자산입니다.
그래서 저는 두 단계로 진행합니다.
1단계 — 확산(fan-out). 저는 새로운 공격 표면(surface)을 여러 개의 독립적인 감사 패스(auditor passes)로 나눕니다. 각 패스는 좁은 권한을 가집니다 ("이 세 파일에서 SSRF만 찾을 것", "이 엔드포인트에서 권한 부여(authz)만 확인할 것"). 하나의 모델이 릴리스 전체를 머릿속에 담고 있는 것보다 좁은 범위(scope)를 다루는 것이 더 효과적입니다. 각 패스는 발견(findings)이 아닌 _후보(candidates)_를 생성합니다.
2단계 — 기본적으로 반박(refute by default). 모든 후보는 그것을 죽이는(kill it) 것이 임무인 별도의 적대적 검증기(adversarial verifier)에게 전달됩니다. 기본 판결은 "이것은 익스플로잇(exploitable)할 수 없음; 내가 틀렸음을 증명하라"입니다. 검증기는 신뢰할 수 없는 소스(untrusted source)에서 위험한 싱크(sink)까지 중간에 방어 기제(guard)가 없는 구체적인 경로를 추적해야 합니다. 즉, 입력을 받는 함수, 호출 체인(call chain), 그리고 정확히 누락된 체크 로직을 찾아내야 합니다. 만약 그 체인을 구축할 수 없다면, 해당 후보는 탈락합니다. "의심스러워 보임"이나 "잠재적으로 가능할 수 있음" 같은 표현은 허용되지 않습니다. 발견 사항은 그것을 부정하려는 적대자가 실패했을 때에만 살아남습니다.
이러한 '기본적 반박' 태도 덕분에 파이프라인을 실제 계정에 적용해도 안전한 것입니다. 확산(fan-out)은 재현율(recall)을 제공하고, 적대적 검증기는 정밀도(precision)를 제공합니다. 당신은 누군가가 적극적으로 폐기하려고 시도했음에도 살아남은 아주 작은 집합만을 제출하게 됩니다.
물러설 줄 아는 규율
여기에 아무도 쓰지 않는 부분이 있습니다: 대부분의 디프는 깨끗하며, 당신은 아무것도 얻지 못할 수도 있다는 사실을 받아들여야 합니다.
두 개의 태그를 고정(pin)하고, 델타(delta)를 가져와서(pull), 전체 파이프라인을 실행했을 때, 정직한 답변은 "새 코드는 괜찮습니다"입니다. 이때 유혹은 엄청납니다. 시간을 투자했으니 보상을 원하게 되고, 결국 약한 후보를 억지로 늘려 보고서로 만들기 시작합니다. 그것이 바로 당신을 불신하도록 플랫폼을 훈련시키는 방식입니다. 억지로 늘린 보고서의 기대값(expected value)은 음수입니다. 보상을 받을 확률은 낮고, 당신의 핸들(handle)을 따라다니며 거절당할 실제 확률은 높기 때문입니다.
깨끗한 디프(diff)를 보고 그냥 지나치는 것은 방법론의 실패가 아닙니다. 그것이 바로 방법론 그 자체입니다. 새로운 코드를 사냥하는 것의 핵심(edge)은, 수많은 작은 델타(delta)를 저렴하게 확인하고 실제로 무언가가 깨졌을 때만 개입하는 것입니다. 보고서의 양이 아니라, 확인(look)의 양이 중요합니다.
이 글이 추상적인 이유 중 하나는, 제가 현재 이와 정확히 동일한 기술을 매우 빠르게 배포되는 인기 있는 RAG 엔진에 적용하여 실행 중이기 때문입니다. 아직 보고되지 않았고 아직 수정되지 않은 결과물들을 말이죠. 따라서 여기에는 구체적인 내용이 전혀 없습니다. 컴포넌트도, 버전도, 페이로드(payload)도 없습니다. 핵심은 프로세스(process)이며, 이 프로세스는 완전히 전이 가능합니다. 새 릴리스를 디프(diff)하고, 새로운 신뢰할 수 없는 입력(untrusted-input) 표면을 매핑하며, 좁은 범위의 감사(audit)를 확산시키고, 기본적으로 모든 것을 반박하며, 살아남은 것들만 제출하고, 깨끗한 것들은 그냥 지나치십시오.
모두가 낚시하는 곳에서 낚시하는 것을 멈추십시오. 코드가 새로운 곳으로 가십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기