모델 서비스 거부 공격 (Model Denial of Wallet): 당신의 돈을 낭비하게 만드는 공격
요약
모델 서비스 거부(Denial of Wallet, DoW) 공격은 시스템을 다운시키는 대신 막대한 API 비용을 발생시켜 재정적 피해를 입히는 공격 방식입니다. 공격자는 저렴한 요청으로 고비용의 롱 컨텍스트 호출을 유도하며, 이를 방어하기 위해 글로벌 상한선 설정과 입력/출력 크기 제한이 필수적입니다.
핵심 포인트
- DoW 공격은 시스템 가용성을 유지하면서 재정적 손실을 유도함
- 무료 티어 및 인증되지 않은 데모가 주요 공격 진입점임
- 입력/출력 토큰 크기 및 비싼 모델 선택을 통한 비용 증폭 가능
- 에이전트 루프와 캐시 무력화는 비용 변동성을 극대화함
- 개별 식별자 제한 외에 글로벌 상한선(Global ceiling) 설정이 중요함
전형적인 서비스 거부 (Denial of Service, DoS) 공격은 시스템을 다운시킵니다. 반면, 모델 서비스 거부 (Denial of Wallet, DoW) 공격은 시스템을 가동된 상태로 유지하며, 완벽하게 응답하면서도 당신이 감당할 수 없는 청구서를 조용히 생성합니다. 오토스케일링 (Autoscaling)과 사용량 기반 과금 체계는 가용성 공격을 재정적 공격으로 변화시켰으며, 이에 대한 방어책 또한 달라져야 합니다.
공격 방식
공격자의 목표는 다운타임이 아니라 지출입니다. 모든 요청은 형식상 정당합니다. 거부할 만한 잘못된 입력도 없고, 흡수해야 할 플러딩 (Flood)도 없습니다. 시스템은 설계된 대로 정확하게 작동하며, 그렇기 때문에 에러율 (Error rates)과 지연 시간 (Latency)을 기반으로 구축된 모니터링 시스템은 아무것도 감지하지 못합니다.
이 공격이 매력적인 이유는 비대칭성이 극단적이기 때문입니다. 공격자에게는 단 몇 백 바이트의 비용도 들지 않는 요청이, 사용자에게는 값비싼 모델에 대한 롱 컨텍스트 (Long-context) 호출 비용을 발생시킬 수 있습니다. OWASP는 2025년 리스트에서 이를 전통적인 자원 고갈 (Resource-exhaustion) 사례와 함께 LLM10의 '무제한 소비 (Unbounded Consumption)' 항목으로 포함시켰습니다.
진입점은 거의 항상 결제가 연결되지 않은 접점입니다: 무료 티어 (Free tier), 인증되지 않은 데모 (Unauthenticated demo), 공개 플레이그라운드 (Public playground), 마케팅 페이지의 채팅 위젯 (Chat widget), 이메일만 있으면 되는 체험판 (Trial) 등이 이에 해당합니다. 이러한 접점들은 의도적으로 마찰이 없도록 설계되어 있는데, 이는 공격자의 비용은 0인 반면 사용자의 비용은 토큰 (Token)당 발생한다는 것을 의미합니다. 카드 결제 없이 접근 가능한 모든 곳에는 개별 식별자 (Identity)당 엄격한 상한선과 해당 접점 전체에 대한 글로벌 상한선 (Global ceiling)이 필요합니다. 그리고 글로벌 상한선이 더 중요합니다. 개별 식별자 제한은 수천 개의 저렴한 가입을 통해 무력화될 수 있기 때문입니다.
실제 청구서가 발생하는 방식
- 볼륨 (Volume). 가장 명백하며, 속도 제한 (Rate limits)이 해결하는 문제입니다.
- 입력 크기 (Input size). 매우 큰 컨텍스트 (Context)를 가진 단일 요청은 수천 개의 일반적인 요청보다 더 많은 비용이 들 수 있습니다. 엔드포인트 (Endpoint)가 문서를 허용한다면, 크기 제한은 비용 제어 수단입니다.
- 출력 크기 (Output size). 출력 토큰 (Output tokens)은 일반적으로 입력 토큰보다 몇 배 더 비쌉니다.
max_tokens제한이 없는 요청은 백지수표와 같으며, 최대 길이의 출력을 요구하는 프롬프트 (Prompt)는 단 한 줄로 수행되는 공격입니다. - 비싼 경로 (Expensive routes). 호출자가 모델을 선택할 수 있다면, 가장 비싼 모델을 선택할 것입니다. 추론 모드 (Reasoning modes)는 사용자가 보지 못하는 토큰을 생성함으로써 토큰 수를 배가시킵니다.
- 에이전트 루프 (Agent loops). 단연코 변동성이 가장 큰 경로입니다. 재시도 루프 (Retry loop)를 유도하는 주입된 콘텐츠, 서로를 호출하는 두 에이전트, 또는 에이전트가 완료할 수 없지만 계속 시도하는 작업 — 모든 반복은 전체 컨텍스트 (Full-context) 호출이며, 히스토리 (History)가 축적됨에 따라 비용은 턴 수의 제곱에 비례하여 증가합니다.
- 캐시 무력화 (Cache defeat). 경제성을 위해 프롬프트 캐싱 (Prompt caching)에 의존한다면, 모든 요청 앞에 무작위 문자열을 추가하는 것만으로 다른 것은 바꾸지 않고 할인 혜택만 제거할 수 있습니다.
- 재시도 증폭 (Retry amplification). 귀하의 자체 재시도 정책 (Retry policy)이 하나의 비싼 실패를 세 번의 실패로 만듭니다. 제공업체가 이미 토큰을 생성한 후에 발생하는 타임아웃 (Timeouts)은 귀하가 폐기한 작업에 대해서도 비용을 청구합니다.
제한을 거는 캡(Caps), 정보를 주는 알림(Alerts)
중요한 차이점은 다음과 같습니다: 캡 (Cap)은 시스템이 수행하는 일을 바꾸고, 알림 (Alert)은 (사람이 깨어 있다면) 결국 사람이 수행하는 일을 바꿉니다. 둘 다 갖출 가치가 있지만, 제어 수단 (Control)인 것은 오직 하나뿐입니다.
| 제어 수단 (Control) | 설명 |
|---|---|
| 하드 지출 캡 (Hard spend cap) | 선택한 기간 동안 키 (Key) 및 계정 (Account)당 제한을 초과하는 요청을 거부합니다. 인간의 개입 없이 작동하는 유일한 제어 수단입니다. 거부된 요청이 무제한 청구서보다 낫다는 것을 사전에 결정해야 합니다. |
| ... |
예산 가드 (A budget guard)
// 호출 전 예상하고, 호출 후 정산하십시오. 예상치는 반드시
// 최악의 경우(max_tokens)를 사용해야 합니다. 그렇지 않으면
// 스트리밍 응답이 요청 수락 당시에는 괜찮아 보였던 예산을 초과할 수 있습니다.
...
예약(Reservation)은 생략하기 쉽지만, 생략했을 때 비용이 많이 발생하는 부분입니다. 예약 없는 '확인 후 호출(check-then-call)' 설계는 백 개의 동시 요청이 모두 동일하게 예산 범위 내에 있는 상태를 관찰하게 만들며, 정작 중요한 순간에 한도가 백 배나 초과되는 결과를 초래합니다.
자가당착적 과다 지출 (The self-inflicted majority)
돈이 실제로 어디로 나가는지에 대해 솔직해져야 합니다. 대부분의 거대한 예상치 못한 청구서는 공격 때문이 아닙니다. 그것은 상한선이 없는 재시도 루프(retry loop), 컨텍스트(context) 크기를 네 배로 늘려버린 프롬프트 템플릿(prompt-template) 변경, 운영 환경을 대상으로 실행된 테스트 스위트(test suite), 모든 요청을 다시 전송하는 디버그 로그(debug log), 또는 작업을 끝내지 못하고 계속 실행되는 에이전트(agent) 때문입니다. 통제 방법은 동일하므로 편리합니다. 공격자를 막기 위해 구축한 통제 장치는 배포 과정의 실수도 잡아낼 것입니다.
두 가지 습관이 대부분의 문제를 해결합니다. 첫째, 작업당 토큰(token)을 지표로 추적하여 코드 리뷰 시 변경 사항에 따른 비용을 가시화하십시오. 둘째, 스테이징(staging) 환경은 폭주하는 루프가 작성되는 곳이므로, 비운영 환경의 한도를 운영 환경보다 더 엄격하게 설정하십시오.
한 가지 더 구축해 둘 만한 가치가 있는 시스템이 있습니다. 모든 호출에 워크로드 식별자(workload identifier) 태그를 달아, 비용을 발생시킨 기능(feature)에 비용을 다시 할당(bill back)하십시오. 이렇게 하면 누군가 필요로 하기 전에 이미 기능별 비용 그래프가 존재하게 됩니다. 이러한 귀속(attribution)이 없다면, 두 배로 늘어난 청구서는 여러 팀에 걸친 일주일간의 조사 대상이 됩니다. 하지만 귀속 체계가 있다면 문제가 된 경로는 1분 만에 명확히 드러나며, 동일한 태그를 통해 계정 전체에 하나의 숫자를 설정하는 대신 기능별로 한도를 설정할 수 있습니다.
한도(Cap)는 지출이 발생하는 지점에 위치해야만 효력을 발휘합니다. 이것이 바로 키별 제한(per-key limit)과 애플리케이션 예산(application budget)이 중복되는 것이 아니라 상호 보완적인 관계인 이유입니다. 즉, 한쪽에서 버그가 발생하더라도 다른 한쪽이 살아남을 수 있습니다. Multigrid는 단순히 지출을 보고하는 것에 그치지 않고, 키별 지출에 대한 엄격한 제한(hard per-key spend limits)을 강제합니다. 보안 사고(compromised case)가 발생했을 때를 대비하여 한도 크기를 설정하십시오. 그리고 올바른 한도 설정의 실패 모드(failure mode)는 요청 거부(refused request)라는 점을 받아들여야 합니다.
관련 항목
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기