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 :
-
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.
-
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.
-
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ère | DoS classique | Denial of Wallet |
|---|---|---|
| Cible | Disponibilité | Budget financier |
| Mesure impact | % uptime | $ consommés |
| Mitigation primaire | Rate limiting + scaling | Cost cap (kill switch) |
| Détection | Monitoring perfs | Monitoring coûts |
| Asymétrie | Attaquant + victime peuvent dépenser | Attaquant peut dépenser 0, victime tout |
| Service pendant attaque | Indisponible | Disponible mais coûte cher |
| Conséquence ultime | Downtime | Faillite (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 existentielMitigation 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 responsePattern 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 attaqueCas 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 capConclusion : sans cost cap, exposition financière potentielle = plusieurs centaines de milliers $ par heure. Justifie investissement défense.
Profils de risque
| Profil | Exposition typique | Priorité défense |
|---|---|---|
| Startup freemium | Critique, risque existentiel | Cost cap dur dès jour 1 |
| SaaS B2B payant | Élevée, impact financier direct | Cost cap par tenant + monitoring |
| Enterprise interne | Modérée, budget IT impacté | Cost cap par projet + alerting |
| Free-tier public démo | Très élevée, abus organisé | Auth + rate limit + cap multiple niveaux |
| Agent autonome (AutoGPT-class) | Très élevée, boucles potentielles | Limits 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 responseCouche 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 tokensTest 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 findingsPhase 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
| Outil | Usage |
|---|---|
| LiteLLM Proxy | Cost tracking + budget caps natifs |
| Portkey | Cost monitoring + caching |
| Langfuse | Cost per session/user/tenant |
| Custom Python | Cost tracker maison (pattern ci-dessus) |
| Cloud billing alerts | AWS/Azure/GCP, alertes budget natifs |
Pour rate limiting
| Outil | Usage |
|---|---|
| Redis | Rate limiter classique (sliding window) |
| API Gateway natif | Cloud (AWS API Gateway, Azure APIM, GCP Endpoints) |
| Kong | API gateway open source avec rate limiting |
| Cloudflare | Edge rate limiting + DDoS protection |
Pour monitoring
| Outil | Usage |
|---|---|
| OpenTelemetry | Standard logs/metrics |
| Prometheus + Grafana | Métriques + dashboards |
| Splunk / Sentinel / Elastic | SIEM standard |
| Langfuse / Phoenix Arize | Observabilité LLM-spécifique |
Anti-patterns récurrents 2024-2025
| Anti-pattern | Symptôme | Fix |
|---|---|---|
| Pas de cost cap dur | Première détection sur facture | Cost cap dur par session/user/global |
| AutoGPT-class default config | Loops infinies en prod | max_steps + max_tool_calls dès jour 1 |
| Free tier sans rate limit | DoW distribué facile | Rate limit + auth + circuit breaker |
| Pas de monitoring runtime | Détection 24-72h post-incident | OTel GenAI + Prometheus + alerting |
| Cost monitoring sans alerting | Métriques visibles mais ignorées | Alerts SOC à 80% / 100% des seuils |
| Pas de mode dégradé | Service down vs cost explose | Fallback FAQ statique / contact humain |
| Limites session sans cap global | User legit + attaque = total débordement | Cost cap multi-niveaux |
| Pas de tests adversariaux DoW | Régression silencieuse | Tests 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 : variableDifférentiel : 900 € vs 90k-450k €. Justifie investissement défense immédiat.
Pour aller plus loin
- Recursive tool-calling : amplification et boucles exploitables, pendant technique.
- Excessive agency : agents IA trop permissions, risque caché, LLM06 voisin.
- Sandboxing agent IA, confinement code execution.
- Auditer un workflow agentique en production, observabilité.
- Fuites de données via LLM en entreprise, autres incidents.
- OWASP LLM Top 10 développeurs, référentiel base.
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.







