LLM Security

Denial of Wallet (DoW) : épuiser le budget API d'un LLM par abus

Denial of Wallet sur LLM : cas AutoGPT, recursive tool calling, prompt amplification, distributed DoW. Mitigations cost cap + circuit breaker + monitoring.

Naim Aouaichia
20 min de lecture
  • LLM10
  • DoW
  • denial of wallet
  • cost
  • rate limiting

Le Denial of Wallet (DoW) est une classe d'attaque qui ne cherche pas à rendre un service indisponible, elle cherche à épuiser le budget financier de la victime. Sur les LLM avec pricing pay-per-token (0-0.09 € par requête selon modèle), le DoW peut transformer une attaque en bombe financière : une startup IA peut voir sa facture passer de 4.5k €/mois normal à 72k € en 6h sous attaque coordonnée. AutoGPT en 2023 a documenté massivement ce phénomène, des développeurs publièrent des factures OpenAI à plusieurs centaines de dollars en heures suite à sessions sans contrôle. Sub-classe d'OWASP LLM10 Unbounded Consumption, le DoW reste sous-estimé en 2026 par la majorité des organisations qui n'ont pas de cost cap dur. Cet article documente les 3 patterns principaux (recursive tool calling, prompt amplification, distributed DoW), les cas réels, les mitigations cumulatives (cost cap + rate limiting + circuit breaker + monitoring) et la méthodologie d'audit.

Pour le pendant technique recursive tool calling : recursive tool-calling : amplification et boucles. Pour excessive agency : excessive agency : agents IA trop permissions.

Pourquoi le DoW est un risque économique réel

Trois propriétés rendent le DoW particulièrement dangereux sur LLM en 2026 :

  1. Pricing pay-per-token amplifie tout : contrairement aux services flat-fee, chaque token consommé = coût direct. 100k tokens = ~4.5-45 € selon modèle. Multiplié par sessions × users × bots = explosion rapide.

  2. Asymétrie attaquant/victime : l'attaquant paie son propre coût d'attaque (potentiellement 0 via free tiers ou comptes compromis), la victime paie le coût des inférences déclenchées. Asymétrie économique en faveur de l'attaquant.

  3. Détection tardive : sans monitoring temps réel, première détection sur la facture, souvent 24-72h après l'attaque, soit trop tard pour limiter les dégâts financiers.

Tip, Pour toute organisation avec service LLM exposé : assumez que le DoW est exploitable. Le test simple : "que se passe-t-il si un user envoie 10000 requêtes en 1h ?". Si réponse = facture explose, vous êtes vulnérable.

DoS classique vs Denial of Wallet

Comparaison

CritèreDoS classiqueDenial of Wallet
CibleDisponibilitéBudget financier
Mesure impact% uptime$ consommés
Mitigation primaireRate limiting + scalingCost cap (kill switch)
DétectionMonitoring perfsMonitoring coûts
AsymétrieAttaquant + victime peuvent dépenserAttaquant peut dépenser 0, victime tout
Service pendant attaqueIndisponibleDisponible mais coûte cher
Conséquence ultimeDowntimeFaillite (worst case) ou suspension service

Exemple chiffré

Scénario : startup IA avec API LLM publique gratuite (free tier 1000 requêtes/jour).

Pricing OpenAI GPT-4o (référence 2025) :
- Input  : ~2.25 € / 1M tokens
- Output : ~9 € / 1M tokens
 
Attaque DoW :
- 1000 bots distribués
- 100 sessions / bot
- Chaque session : 50k tokens input + 50k tokens output
- Total : 1000 × 100 × 100k = 10 milliards tokens
- Coût : ~45k € en 6 heures
 
Pour startup avec runway 6 mois × 45k €/mois budget :
→ DoW = 6h pour vider 1 mois de budget
→ Risque existentiel

Mitigation simple : cost cap dur à 0.9k €/jour total = attaque limitée à 0.9k € avant kill switch.

Les 3 patterns principaux DoW LLM

Pattern 1, Recursive tool calling (boucle infinie)

Mécanique : agent IA en boucle infinie consomme tokens à chaque tour.

Agent ReAct :
Tour 1 : "Je dois analyser X" → tool call A → output A
Tour 2 : "Je dois analyser X plus" → tool call A → output A'
Tour 3 : "Je dois analyser X encore" → tool call A → output A''
...
Tour 100 : context = 100k tokens, coût = ~4.5 €
Tour 1000 : context = 1M tokens, coût = ~45 €

Cas AutoGPT 2023 : objectif vague ("build me a startup") → agent boucle indéfiniment sur recherche web → résumé → réflexion → nouvelle recherche.

Témoignages publics 2023 :

  • Reddit user 1 : facture OpenAI 78 € en 3h (session AutoGPT non monitorée).
  • Reddit user 2 : 220 € en 8h.
  • GitHub issues multiples : "I left AutoGPT running overnight, cost 540 €".

Voir recursive tool-calling pour le détail technique.

Pattern 2, Prompt amplification

Mécanique : input petit → context grandissant → coût quadratique.

# Pattern dangereux
def chat_naive(user_message: str, history: list) -> str:
    history.append({"role": "user", "content": user_message})
    
    # PAS de cap sur history → context grossit chaque tour
    response = llm.complete(messages=history, model="gpt-4o")
    
    history.append({"role": "assistant", "content": response})
    return response
 
# Coût quadratique :
# Tour 1  : 100 tokens input  →  ~0 €
# Tour 10 : 1k tokens input   →  ~0.01 €
# Tour 100: 100k tokens input →  ~0.9 €
# Tour 1000: 1M tokens input  →  ~9 €

Mitigation : cap context size avec sliding window ou summarization.

def chat_safe(user_message: str, history: list) -> str:
    history.append({"role": "user", "content": user_message})
    
    # Sliding window, garder seulement N derniers tours
    MAX_TOURS = 20
    if len(history) > MAX_TOURS:
        # Summary des anciens tours + N derniers
        old_summary = summarize_old_tours(history[:-MAX_TOURS])
        history = [{"role": "system", "content": f"Previous context: {old_summary}"}] + history[-MAX_TOURS:]
    
    response = llm.complete(messages=history, model="gpt-4o")
    history.append({"role": "assistant", "content": response})
    return response

Pattern 3, Distributed DoW

Mécanique : multiples requêtes coordonnées (botnet, comptes compromis, free-tier abuse) sur cible.

Vecteurs typiques :
- 1000 bots Cloudflare/Akamai bypass via résiduals
- Comptes free-tier compromis (credentials leaks)
- API keys leakées dans GitHub publics
- Browser-based attacks via XSS injection (déclenche LLM côté serveur victime)
 
Multiplicateur :
- 1 attaquant = 0.9k €/h max attaque
- 1000 bots distribués = 0.9M €/h max attaque

Cas anonymisés 2024-2025 :

  • Startup AI freemium : campagne 1000 bots → 72k € en 6h.
  • SaaS LLM-powered : free tier saturé puis bascule paid tier triggered par bypass auth → coût explosé.

Mitigation : rate limiting par IP + par compte + global + circuit breaker au niveau API gateway.

Cas publics 2023-2025

AutoGPT cost explosions (mars-décembre 2023)

Contexte : AutoGPT (open source, Toran Bruce Richards) a explosé en popularité au printemps 2023.

Configuration par défaut :

  • Aucun cap budget.
  • Aucun max_steps.
  • Aucun max_session_duration.
  • Loops illimités ReAct → recherche → résumé → réflexion.

Pattern documenté :

User démarre AutoGPT avec objectif vague.
Agent boucle :
  - search_web("requête vague")
  - read_file("résultat")
  - summarize(résultat)
  - reflect(synthèse)
  - search_web("nouvelle requête basée sur réflexion")
  - ...
Sans condition de sortie claire → boucle infinie.
Consommation : 100k+ tokens / session.

Témoignages publics 2023 :

  • Reddit r/AutoGPT : centaines de threads "How much did your run cost?"
  • GitHub issues : multiples "Cost spike, left running overnight".
  • Twitter : threads viraux sur factures à 4 chiffres.

Réponse communautaire :

  • Issues open source pour ajouter max_steps (mergé fin 2023).
  • Mode "dry run" ajouté.
  • Documentation mise à jour avec warnings.

Leçon : defaults pas safe = problème massif à grande échelle. AutoGPT en core open source ne propose toujours pas de limites strictes par défaut en 2026, c'est au déployeur d'en ajouter.

BabyAGI et autres agents 2023-2024

BabyAGI (Yohei Nakajima) : pattern similaire, autonomous agent loops sans cap.

AgentGPT (web-based AutoGPT clone) : factures explosées partagées publiquement.

LangChain AgentExecutor : multiples issues GitHub 2023-2024 sur loops + cost.

Pattern récurrent : agents éducationnels/démos sans limites strictes → utilisateurs découvrent en production que c'est un problème.

Disclosures responsables 2024-2025 (anonymisés)

Cas A, Startup AI freemium :

  • Service LLM-powered freemium.
  • Free tier 1000 requêtes/jour, payant au-delà.
  • Attaque coordonnée 1000 bots compromis CAPTCHA bypass.
  • Bascule automatique vers paid tier (config faille).
  • Facture OpenAI : 4.5k €/mois → 72k € en 6h.
  • Sauvée par alerting interne + circuit breaker activé manuellement.

Cas B, SaaS chatbot enterprise :

  • API gateway sans cost cap.
  • Customer compromis utilisé pour exfiltration.
  • Volume requêtes × 100 sur 24h non détecté.
  • Facture mensuelle x 5 vs normale.
  • Pas de plan de continuité, service degraded 1 semaine.

Cas C, Plateforme LLM pour gouvernement :

  • Recursive tool calling déclenché par bug interne (pas attaque).
  • Agent boucle sur tool de recherche pendant 8h.
  • Coût 12k€ en une nuit (limite contractuelle Azure OpenAI heureusement).
  • Investigation post-mortem 2 semaines.

Calcul du risque DoW

Modèle de risque simple

Risque DoW (€/h) = max_RPS × tokens_per_request × prix_per_token
 
Exemple GPT-4o :
- max_RPS sans rate limit : 1000 req/s
- tokens_per_request moyen : 5000 (input + output)
- prix moyen : ~4.5 € / 1M tokens
- Risque DoW : 1000 × 5000 × 5/1M = 25 $/s = 81k €/h
 
Exemple Claude Sonnet 4.6 :
- Pricing similaire
- Risque DoW : ~72k-90k €/h sans cap

Conclusion : sans cost cap, exposition financière potentielle = plusieurs centaines de milliers $ par heure. Justifie investissement défense.

Profils de risque

ProfilExposition typiquePriorité défense
Startup freemiumCritique, risque existentielCost cap dur dès jour 1
SaaS B2B payantÉlevée, impact financier directCost cap par tenant + monitoring
Enterprise interneModérée, budget IT impactéCost cap par projet + alerting
Free-tier public démoTrès élevée, abus organiséAuth + rate limit + cap multiple niveaux
Agent autonome (AutoGPT-class)Très élevée, boucles potentiellesLimits strictes par défaut

Mitigations cumulatives

Couche 1, Cost cap dur

Le seul vrai filet de sécurité. Tout le reste = best effort, le cost cap = kill switch garanti.

import time
from typing import Literal
 
class CostTracker:
    """Cost tracking avec hard kill switch."""
    
    def __init__(self):
        self.daily_budgets = {
            "global": 1000.0,           # 900 €/jour total
            "per_user": 10.0,           # 9 €/jour/user
            "per_session": 1.0,         # 0.9 €/session
            "per_tenant": 100.0,        # 90 €/jour/tenant
        }
        self.daily_consumption = defaultdict(float)
    
    def check_and_record(self, scope: str, scope_id: str, estimated_cost: float):
        """Check si budget OK, record si oui, raise si non."""
        key = f"{scope}:{scope_id}:{date.today().isoformat()}"
        current = self.daily_consumption[key]
        budget = self.daily_budgets[scope]
        
        if current + estimated_cost > budget:
            log_event("cost_cap_reached", scope=scope, scope_id=scope_id, current=current, attempted=estimated_cost)
            raise BudgetExceeded(f"{scope} budget exceeded: {current}/{budget}")
        
        self.daily_consumption[key] += estimated_cost
        
        # Alerte SOC à 80%
        if current / budget > 0.8 and (current + estimated_cost) / budget > 0.8:
            alert_soc("cost_warning_80pct", scope=scope, scope_id=scope_id)
        
        return True
 
cost_tracker = CostTracker()
 
async def safe_llm_call(prompt: str, user_id: str, session_id: str, tenant_id: str):
    estimated_cost = estimate_cost(prompt, model="gpt-4o")
    
    # 4 niveaux de check (tous doivent passer)
    cost_tracker.check_and_record("global", "all", estimated_cost)
    cost_tracker.check_and_record("per_user", user_id, estimated_cost)
    cost_tracker.check_and_record("per_session", session_id, estimated_cost)
    cost_tracker.check_and_record("per_tenant", tenant_id, estimated_cost)
    
    response = await llm.complete(prompt)
    
    # Update avec coût réel post-call
    actual_cost = compute_actual_cost(response, model="gpt-4o")
    diff = actual_cost - estimated_cost
    if diff > 0:
        for scope in ["global", "per_user", "per_session", "per_tenant"]:
            scope_id = {"global": "all", "per_user": user_id, "per_session": session_id, "per_tenant": tenant_id}[scope]
            cost_tracker.daily_consumption[f"{scope}:{scope_id}:{date.today().isoformat()}"] += diff
    
    return response

Couche 2, Rate limiting progressif

from datetime import datetime, timedelta
 
class ProgressiveRateLimiter:
    """Rate limiter avec dégradation progressive."""
    
    LIMITS = {
        "per_minute": {"requests": 60, "tokens": 50_000},
        "per_hour": {"requests": 1000, "tokens": 500_000},
        "per_day": {"requests": 10_000, "tokens": 5_000_000},
    }
    
    def __init__(self):
        self.consumption = defaultdict(lambda: defaultdict(int))
    
    def check(self, user_id: str, tokens_estimate: int) -> bool:
        now = datetime.utcnow()
        
        for window, limits in self.LIMITS.items():
            window_key = self._window_key(now, window)
            current_req = self.consumption[user_id][f"{window}:{window_key}:req"]
            current_tok = self.consumption[user_id][f"{window}:{window_key}:tok"]
            
            if current_req >= limits["requests"]:
                log_event("rate_limit_req", user=user_id, window=window)
                raise RateLimitExceeded(window, "requests")
            
            if current_tok + tokens_estimate > limits["tokens"]:
                log_event("rate_limit_tok", user=user_id, window=window)
                raise RateLimitExceeded(window, "tokens")
        
        # OK, record
        for window in self.LIMITS:
            window_key = self._window_key(now, window)
            self.consumption[user_id][f"{window}:{window_key}:req"] += 1
            self.consumption[user_id][f"{window}:{window_key}:tok"] += tokens_estimate
        
        return True
    
    def _window_key(self, now: datetime, window: str) -> str:
        if window == "per_minute":
            return now.strftime("%Y%m%d%H%M")
        elif window == "per_hour":
            return now.strftime("%Y%m%d%H")
        elif window == "per_day":
            return now.strftime("%Y%m%d")

Couche 3, Limites strictes par session/agent

@dataclass
class AgentLimits:
    """Limites strictes par session agent."""
    max_steps: int = 25
    max_tool_calls: int = 50
    max_session_seconds: int = 300
    max_cost_usd: float = 1.0
    max_same_tool_consecutive: int = 5  # détection boucle
    max_tokens_total: int = 200_000
 
class SessionLimitGuard:
    def __init__(self, limits: AgentLimits, session_id: str):
        self.limits = limits
        self.session_id = session_id
        self.steps = 0
        self.tool_calls = 0
        self.consecutive_same_tool = 0
        self.last_tool = None
        self.start_time = time.time()
        self.cost_usd = 0.0
        self.tokens = 0
    
    def check_step(self):
        if self.steps >= self.limits.max_steps:
            self._kill("max_steps")
        if time.time() - self.start_time > self.limits.max_session_seconds:
            self._kill("max_session_seconds")
        if self.cost_usd >= self.limits.max_cost_usd:
            self._kill("max_cost_usd")
        if self.tokens >= self.limits.max_tokens_total:
            self._kill("max_tokens_total")
        
        self.steps += 1
    
    def on_tool_call(self, tool_name: str):
        self.tool_calls += 1
        if self.tool_calls > self.limits.max_tool_calls:
            self._kill("max_tool_calls")
        
        if tool_name == self.last_tool:
            self.consecutive_same_tool += 1
            if self.consecutive_same_tool >= self.limits.max_same_tool_consecutive:
                self._kill("loop_detected_same_tool")
        else:
            self.consecutive_same_tool = 1
            self.last_tool = tool_name
    
    def _kill(self, reason: str):
        log_event("session_killed", session=self.session_id, reason=reason, cost=self.cost_usd)
        alert_soc("session_kill_switch", session=self.session_id, reason=reason)
        raise BudgetExceeded(reason)

Couche 4, Circuit breaker

from collections import deque
 
class APIGatewayCircuitBreaker:
    """Circuit breaker au niveau API gateway global."""
    
    def __init__(self):
        self.recent_failures = deque(maxlen=100)
        self.failure_threshold = 10  # 10 incidents en 5 min = OPEN
        self.window_seconds = 300
        self.state = "CLOSED"  # CLOSED | OPEN | HALF_OPEN
        self.opened_at = None
    
    def record_failure(self, reason: str):
        self.recent_failures.append((time.time(), reason))
        self._purge()
        
        if self.state == "CLOSED" and len(self.recent_failures) >= self.failure_threshold:
            self._open()
    
    def _purge(self):
        cutoff = time.time() - self.window_seconds
        while self.recent_failures and self.recent_failures[0][0] < cutoff:
            self.recent_failures.popleft()
    
    def _open(self):
        self.state = "OPEN"
        self.opened_at = time.time()
        log_event("circuit_breaker_opened", failures=list(self.recent_failures))
        alert_soc("circuit_breaker_opened", count=len(self.recent_failures))
    
    def is_open(self) -> bool:
        if self.state == "OPEN":
            # Try transition to HALF_OPEN après 60s
            if time.time() - self.opened_at > 60:
                self.state = "HALF_OPEN"
                return False  # autoriser un test
            return True
        return False
    
    def record_success(self):
        if self.state == "HALF_OPEN":
            self.state = "CLOSED"
            self.recent_failures.clear()
            log_event("circuit_breaker_closed")

Couche 5, Monitoring runtime + alerting

# Métriques à pusher au SIEM via OTel GenAI semantic conventions
 
METRICS_TO_TRACK = {
    # Volume
    "gen_ai.requests.per_minute": {"warning": 1000, "critical": 5000},
    "gen_ai.tokens.per_minute": {"warning": 500_000, "critical": 2_000_000},
    
    # Cost
    "gen_ai.cost.per_minute": {"warning": 10.0, "critical": 50.0},
    "gen_ai.cost.per_user.per_hour": {"warning": 5.0, "critical": 20.0},
    "gen_ai.cost.per_session": {"warning": 0.5, "critical": 2.0},
    
    # Patterns suspects
    "agent.same_tool_consecutive": {"warning": 3, "critical": 5},
    "agent.session_duration_seconds": {"warning": 240, "critical": 300},
    "agent.tool_calls_per_session": {"warning": 30, "critical": 50},
}
 
def emit_metrics(metric: str, value: float, labels: dict):
    """Émet vers OTel collector → Prometheus → Grafana + SIEM."""
    otel_meter.create_gauge(metric).set(value, labels)
    
    # Check thresholds
    if metric in METRICS_TO_TRACK:
        thresholds = METRICS_TO_TRACK[metric]
        if value >= thresholds["critical"]:
            alert_soc("metric_critical", metric=metric, value=value, labels=labels)
        elif value >= thresholds["warning"]:
            alert_soc("metric_warning", metric=metric, value=value, labels=labels)

Couche 6, Plan de continuité (mode dégradé)

Pour services critiques : mode dégradé sans LLM si circuit breaker activé ou budget atteint.

async def chat_with_fallback(user_message: str, user_session):
    """Chat avec fallback vers mode dégradé si nécessaire."""
    
    if circuit_breaker.is_open():
        return await degraded_response(user_message, user_session)
    
    if cost_tracker.is_near_budget("global", "all", threshold=0.95):
        # Budget global > 95%, mode dégradé pour tous
        return await degraded_response(user_message, user_session)
    
    try:
        response = await llm.complete(user_message)
        return response
    except (BudgetExceeded, RateLimitExceeded):
        return await degraded_response(user_message, user_session)
 
async def degraded_response(user_message: str, user_session):
    """Mode dégradé sans LLM."""
    # 1. Recherche FAQ statique
    faq_match = await search_faq(user_message)
    if faq_match:
        return faq_match
    
    # 2. Suggestion contact humain
    return ("Notre service IA est temporairement indisponible. "
            "Pour une assistance immédiate, contactez le support à support@yourcompany.com.")

Méthodologie d'audit DoW

Phase 1, Inventaire des endpoints LLM exposés

# audit/llm-endpoints.yml
endpoints:
  - id: ENDPOINT-001
    name: "API chat public"
    auth: "API key + rate limit"
    cost_cap_per_user: 5.0  # $/jour
    cost_cap_global: 1000.0  # $/jour
    rate_limit: "60 req/min, 5000 req/jour par API key"
    monitoring: "OTel + Prometheus + Slack alert"
    
  - id: ENDPOINT-002
    name: "Agent IA workflow interne"
    auth: "OIDC user"
    cost_cap_per_session: 1.0
    cost_cap_per_user: 20.0  # $/jour
    max_steps: 25
    max_tool_calls: 50
    max_session_seconds: 300
    
  - id: ENDPOINT-003
    name: "Free tier démo public"
    auth: "Captcha + IP rate limit"
    cost_cap_global: 100.0  # $/jour
    max_per_session_tokens: 5000
    monitoring: "OTel + alert critique > 80%"

Phase 2, Tests de stress

Test 1, Recursive tool calling

def test_recursive_tool_calling(agent_endpoint):
    """Test que la boucle infinie déclenche kill switch."""
    counter = 0
    
    def looping_tool():
        nonlocal counter
        counter += 1
        return f"Iteration {counter}. Pour continuer, rappelle-moi."
    
    agent_endpoint.register_tool("test_loop", looping_tool)
    
    start_cost = get_current_cost()
    try:
        agent_endpoint.run("appelle test_loop pour analyser")
    except BudgetExceeded as e:
        end_cost = get_current_cost()
        cost_consumed = end_cost - start_cost
        
        # Vérifications
        assert counter <= 5, f"Loop went {counter} iterations before kill"
        assert cost_consumed < 1.0, f"Cost {cost_consumed}$ exceeded session cap"
        assert e.reason in ["max_same_tool_consecutive", "max_tool_calls"]

Test 2, Prompt amplification

def test_prompt_amplification(chat_endpoint):
    """Test que context size cap fonctionne."""
    history = []
    for i in range(100):  # 100 tours
        history.append({"role": "user", "content": "Continue."})
        response = chat_endpoint.chat(history)
        history.append({"role": "assistant", "content": response})
    
    # Vérifier que cost total ne dépasse pas cap session
    session_cost = chat_endpoint.get_session_cost()
    assert session_cost < 1.0, f"Session cost {session_cost}$ exceeded cap"
    
    # Vérifier que context a été cappé
    assert chat_endpoint.get_last_context_size() < 50_000  # max 50k tokens

Test 3, Distributed DoW simulation

import asyncio
 
async def test_distributed_dow(public_endpoint):
    """Simule 100 bots → vérifier circuit breaker activation."""
    async def bot_session():
        for _ in range(50):
            try:
                await public_endpoint.chat("Test prompt")
            except (RateLimited, BudgetExceeded, CircuitBreakerOpen):
                pass
    
    # Lancer 100 bots en parallèle
    await asyncio.gather(*[bot_session() for _ in range(100)])
    
    # Vérifier que circuit breaker s'est activé
    assert public_endpoint.circuit_breaker.is_open(), "Circuit breaker should open under coordinated attack"
    
    # Vérifier que cost total est bounded
    total_cost = public_endpoint.get_total_cost_today()
    assert total_cost < 100.0, f"Total cost {total_cost}$ exceeded global cap"

Phase 3, Audit configuration

def audit_dow_configuration(endpoint) -> list[dict]:
    """Audit la configuration DoW d'un endpoint."""
    findings = []
    
    config = endpoint.get_config()
    
    # Cost caps présents ?
    if not config.get("cost_cap_global"):
        findings.append({"severity": "critical", "issue": "No global cost cap"})
    if not config.get("cost_cap_per_user"):
        findings.append({"severity": "high", "issue": "No per-user cost cap"})
    if not config.get("cost_cap_per_session"):
        findings.append({"severity": "high", "issue": "No per-session cost cap"})
    
    # Limites agent ?
    if endpoint.is_agent():
        if not config.get("max_steps"):
            findings.append({"severity": "critical", "issue": "Agent without max_steps"})
        if not config.get("max_tool_calls"):
            findings.append({"severity": "critical", "issue": "Agent without max_tool_calls"})
        if not config.get("max_same_tool_consecutive"):
            findings.append({"severity": "high", "issue": "No loop detection"})
    
    # Rate limiting ?
    if not config.get("rate_limit"):
        findings.append({"severity": "critical", "issue": "No rate limiting"})
    
    # Circuit breaker ?
    if not config.get("circuit_breaker_enabled"):
        findings.append({"severity": "high", "issue": "No circuit breaker"})
    
    # Monitoring ?
    if not config.get("cost_monitoring_active"):
        findings.append({"severity": "critical", "issue": "No cost monitoring"})
    
    return findings

Phase 4, Tests détection / alerting

def test_alerting_on_spike(endpoint):
    """Vérifier que SOC est alerté en cas de spike cost."""
    # Reset alerts
    soc_listener.clear()
    
    # Générer spike artificiel
    for _ in range(1000):
        try:
            endpoint.chat("Test")
        except:
            pass
    
    # Attendre alerts
    time.sleep(5)
    
    alerts = soc_listener.get_alerts()
    assert any(a.type == "cost_spike" for a in alerts), "No cost spike alert generated"
    assert any(a.severity in ["warning", "critical"] for a in alerts)

Outils opérationnels

Pour cost tracking et caps

OutilUsage
LiteLLM ProxyCost tracking + budget caps natifs
PortkeyCost monitoring + caching
LangfuseCost per session/user/tenant
Custom PythonCost tracker maison (pattern ci-dessus)
Cloud billing alertsAWS/Azure/GCP, alertes budget natifs

Pour rate limiting

OutilUsage
RedisRate limiter classique (sliding window)
API Gateway natifCloud (AWS API Gateway, Azure APIM, GCP Endpoints)
KongAPI gateway open source avec rate limiting
CloudflareEdge rate limiting + DDoS protection

Pour monitoring

OutilUsage
OpenTelemetryStandard logs/metrics
Prometheus + GrafanaMétriques + dashboards
Splunk / Sentinel / ElasticSIEM standard
Langfuse / Phoenix ArizeObservabilité LLM-spécifique

Anti-patterns récurrents 2024-2025

Anti-patternSymptômeFix
Pas de cost cap durPremière détection sur factureCost cap dur par session/user/global
AutoGPT-class default configLoops infinies en prodmax_steps + max_tool_calls dès jour 1
Free tier sans rate limitDoW distribué facileRate limit + auth + circuit breaker
Pas de monitoring runtimeDétection 24-72h post-incidentOTel GenAI + Prometheus + alerting
Cost monitoring sans alertingMétriques visibles mais ignoréesAlerts SOC à 80% / 100% des seuils
Pas de mode dégradéService down vs cost exploseFallback FAQ statique / contact humain
Limites session sans cap globalUser legit + attaque = total débordementCost cap multi-niveaux
Pas de tests adversariaux DoWRégression silencieuseTests CI réguliers

Mapping aux frameworks

OWASP

  • LLM10 Unbounded Consumption : catégorie centrale (renommée v2 2025 spécifiquement pour cette classe).
  • LLM06 Excessive Agency : adjacent (autonomy excessive amplifie DoW).
  • LLM01 Prompt Injection : vecteur courant pour déclencher DoW (attaquant injecte prompt qui cause amplification).

MITRE ATLAS

  • AML.T0029 Denial of ML Service.
  • AML.T0024 Exfiltration via ML Inference API (volume).

EU AI Act

  • Article 15 (cybersécurité robustesse), applicable.

NIST AI RMF

  • Manage : runbook DoW.
  • Measure : KPI cost / MTTR.

Coût économique des incidents DoW

Cas worst-case

Attaque DoW non détectée pendant 24h sur startup AI :
- Volume : 1M requêtes en 24h
- Tokens moyens : 5000 par requête
- Pricing : ~4.5 € / 1M tokens
- Coût : 25 0 €
 
Si attaque coordonnée 1000 bots :
- Volume : 1B requêtes en 24h (théorique)
- Coût : 22.5M €, probable saturation cap fournisseur
- Réalité : limite cap fournisseur (Azure/OpenAI) typiquement 90k-450k €/mois
 
Worst case réaliste : 90k-450k € avant cap fournisseur ne stoppe.

Cas mitigé

Avec cost cap dur 900 €/jour :
- Attaque DoW détectée à 900 € → kill switch
- Coût total : 900 €
- Service degradé pour 1 jour
- Coût indirect : variable

Différentiel : 900 € vs 90k-450k €. Justifie investissement défense immédiat.

Pour aller plus loin

Points clés à retenir

  • Denial of Wallet (DoW) = sub-classe d'OWASP LLM10 Unbounded Consumption visant l'épuisement budget plutôt que la disponibilité.
  • 3 patterns principaux : recursive tool calling (boucles agents), prompt amplification (context grandissant), distributed DoW (botnet coordonné).
  • Cas réels : AutoGPT cost explosions 2023 (factures 90-540 €/session), BabyAGI/AgentGPT 2023-2024, disclosures responsables 2024-2025 sur SaaS LLM.
  • Asymétrie attaquant/victime : attaquant peut payer 0 (free tiers, comptes compromis), victime paie le coût des inférences.
  • Sans cost cap, exposition financière potentielle = plusieurs centaines de milliers $/h.
  • Mitigation en 6 couches : cost cap dur (kill switch), rate limiting progressif, limites strictes session/agent, circuit breaker, monitoring runtime + alerting, plan de continuité (mode dégradé).
  • Cost cap dur = la seule vraie sécurité. Tout le reste = best effort.
  • Audit en 4 phases : inventaire endpoints → tests stress → audit configuration → tests alerting.
  • Outils : LiteLLM Proxy (cost tracking natif), Langfuse (cost monitoring), Cloudflare (rate limiting edge), OTel + Prometheus + SIEM (monitoring), Redis (rate limiter custom).
  • 8 anti-patterns dominants : pas de cost cap, AutoGPT-class defaults, free tier sans rate limit, pas de monitoring, cost monitoring sans alerting, pas de mode dégradé, limites sans cap global, pas de tests CI.

Le DoW reste sous-estimé en 2026. Beaucoup d'organisations découvrent le problème sur la facture, pas en preventive. Investir dans cost cap dur + monitoring + circuit breaker = un des meilleurs ROI sécurité IA, particulièrement pour startups et services freemium où le risque peut être existentiel.

Questions fréquentes

  • Qu'est-ce que le Denial of Wallet (DoW) appliqué aux LLM ?
    Le **Denial of Wallet** est une attaque qui ne cherche pas à rendre un service indisponible (DoS classique) mais à **épuiser le budget** financier de la victime. Sur LLM, le coût par token de l'inférence (typiquement 0-0.09 € par requête selon modèle + taille) crée un vecteur d'abus économique. Trois patterns principaux : (1) **Recursive tool calling** (boucle qui consomme tokens), (2) **Prompt amplification** (input petit → context massif), (3) **Distributed DoW** (multiples requêtes coordonnées sur compte cible). Cas réel : AutoGPT 2023, plusieurs développeurs publièrent des factures OpenAI à plusieurs centaines de dollars en heures. Sub-classe de OWASP LLM10 *Unbounded Consumption*.
  • Quelle différence entre DoS classique et Denial of Wallet ?
    **DoS classique** : cible la **disponibilité**, saturer ressources (CPU, RAM, bande passante) pour rendre service indisponible. Mitigation : rate limiting, scaling. **Denial of Wallet** : cible le **budget financier**, exploiter le pricing pay-per-use des API LLM pour augmenter les coûts de la victime. Le service reste disponible (tant que budget tient), mais coût explose. Mitigation : **cost cap** (kill switch budget), pas seulement rate limiting. Pour services freemium/free-tier : DoW peut basculer service vers tier gratuit saturé. Pour services payants : DoW peut consommer budget mensuel en heures, forcer suspension de service.
  • Quels sont les vrais cas publics de DoW sur LLM ?
    (1) **AutoGPT cost explosions 2023** : multiples développeurs ont publié leurs factures OpenAI suite à sessions AutoGPT sans contrôle. Reddit/GitHub témoignages, 100k+ tokens consommés par session, factures plusieurs centaines de dollars en heures. (2) **BabyAGI / AgentGPT 2023-2024** : pattern similaire, agents en boucle infinie. (3) **Recursive tool calling** sur LangChain agents production, multiples disclosures responsables 2024-2025. (4) **Distributed DoW** émergent 2025 sur SaaS LLM-powered : campagnes de bots ciblant comptes free-tier ou enterprise mal protégés. Pas de CVE formelle DoW LLM en 2026 mais classe documentée massivement dans communauté. Sub-classe OWASP LLM10 *Unbounded Consumption*.
  • Comment se protéger contre le Denial of Wallet ?
    Cinq couches cumulatives. (1) **Cost cap par session/user/day** : kill switch automatique si dépassement seuil. (2) **Rate limiting progressif** : quotas avec dégradation graceful. (3) **Limites strictes agent** (max_steps, max_tool_calls, max_session_seconds, max_cost_per_session). (4) **Circuit breaker** : si trop d'incidents récents, blocage temporaire nouvelles sessions. (5) **Monitoring runtime** : alertes SOC sur burst cost, patterns suspects (volume + diversité sémantique élevée = extraction probable). **Outils** : LiteLLM Proxy avec budget tracking, Langfuse cost monitoring, OTel GenAI + Prometheus pour alertes. Sans ces mesures, **première détection se fera sur la facture**, trop tard.
  • Le DoW peut-il faire faillir une startup IA ?
    Oui, théoriquement et pratiquement. **Risque réel** pour startups freemium ou avec budgets serrés. Une attaque DoW coordonnée (1000 bots × 100 sessions × 100k tokens × 0.05 €/1k tokens) = **450k €+ en quelques heures**. Pour une startup avec runway limité : potentiel problème existentiel. **Mitigations critiques** : cost cap **dur** (hard limit, pas soft warning), circuit breaker au niveau API gateway (LiteLLM Proxy peut le faire nativement), monitoring temps réel + alertes SOC, plan de continuité (mode dégradé sans LLM). **Témoignage anonymisé** : startup AI 2024 a vu sa facture OpenAI passer de 4.5k €/mois normal à 72k € en 6h suite à campagne bots, sauvée par alerting + circuit breaker activé manuellement après 2h de réaction.
  • Comment auditer une app LLM contre le DoW ?
    Méthodologie 5 phases. (1) **Audit limits configuration** : vérifier que max_steps, max_tool_calls, max_session_seconds, max_cost sont configurés et appliqués. (2) **Test recursive tool calling** : créer un tool de test qui retourne 'rappelle-moi'. Vérifier que limit déclenche kill switch après N appels. (3) **Test prompt amplification** : input petit → vérifier context size cap. (4) **Test cost monitoring** : injecter 1000 requêtes consécutives, vérifier que monitoring détecte + alerte SOC. (5) **Test distributed DoW simulation** : simuler 100 bots, vérifier que circuit breaker active. **Outils** : Garak avec probes spécifiques cost, scripts custom, Langfuse pour mesure cost, Prometheus pour alertes.

Écrit par

Naim Aouaichia

Cyber Security Engineer et fondateur de Zeroday Cyber Academy

Ingénieur cybersécurité avec un parcours hybride : développement, DevOps Capgemini, DevSecOps IN Groupe (sécurité des documents d'identité régaliens), audits CAC 40. Fondateur de Hash24Security et Zeroday Cyber Academy. Présence LinkedIn 43 000 abonnés, Substack Zeroday Notes 23 000 abonnés.