Limitation du débit

La limitation du débit est un mécanisme de défense fondamental pour protéger votre système contre les abus d'API, les attaques DoS et le scraping.

Comparaison des algorithmes

AlgorithmeCaractéristiquesPourCons
Fixed WindowComptage dans une fenêtre de temps fixeSimple à mettre en œuvre, efficace en termes de mémoireLes rafales se produisent aux limites des fenêtres
Sliding Window LogHorodatage des demandes d'enregistrementUn contrôle précisConsommation élevée de mémoire
Sliding Window CounterMoyenne pondérée de la fenêtre précédente et de la fenêtre actuelleBon équilibreAssez complexe
Token BucketConsomme des jetons pour l'accèsPermet des rafales tout en imposant des limitesNécessite un réglage des paramètres
Leaky BucketTraite les demandes à un rythme constantTaux de sortie stableMauvaise gestion des rafales

Mise en œuvre de la limitation de vitesse dans Express.js

JavaScript (Express)Limitation de débit multicouche
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');

// Limite globale : appliquée à toutes les API
const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,
  standardHeaders: true,    // Retourne les en-têtes RateLimit-*.
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

// Pour les points finaux d'authentification : limites plus strictes
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // Tentatives de connexion limitées à 5 par 15 minutes
  skipSuccessfulRequests: true, // Les demandes réussies ne sont pas comptabilisées
});

// Pour les environnements distribués : Backend Redis
const distributedLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redisClient.sendCommand(args) }),
  windowMs: 60 * 1000,
  max: 30,
});

app.use(globalLimiter);
app.use('/api/auth', authLimiter);

En-têtes de réponse

HTTP Response HeadersRFC 6585 / draft-ietf-httpapi-ratelimit-headers
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1672531200
Retry-After: 30
Touches de limitation du débit

En appliquant des limites de taux à l'aide de clés composites telles que la clé API, l'ID utilisateur ou le point de terminaison en plus de l'adresse IP, vous pouvez minimiser l'impact sur les utilisateurs légitimes.

🔍 Validation des entrées

Toutes les entrées de l'API peuvent contenir des données malveillantes. Il faut toujours valider du côté du serveur.

Principes de validation

Approche par liste d'attente

Définir une liste de valeurs autorisées. Plus sûr qu'une liste de refus (liste de blocage). Peut gérer de nouveaux modèles d'attaque.

Type, longueur et portée

Vérifier strictement les types de données, le nombre maximal de caractères et les limites inférieures et supérieures des nombres. Il s'agit d'une défense fondamentale contre les débordements de mémoire tampon.

Désinfecter ou rejeter

Il est plus sûr de rejeter les données non valides que de les "réparer". L'assainissement peut produire des transformations inattendues.

Validation avec le schéma JSON

JavaScript (Express + Ajv)Validation du schéma
const Ajv = require('ajv');
const addFormats = require('ajv-formats');

const ajv = new Ajv({ allErrors: true, removeAdditional: true });
addFormats(ajv);

// Schéma pour l'API de création d'utilisateurs
const createUserSchema = {
  type: 'object',
  required: ['name', 'email'],
  additionalProperties: false,
  properties: {
    name: {
      type: 'string',
      minLength: 1,
      maxLength: 100,
      pattern: '^[a-zA-Z0-9\\s\\-]+$', // Restreindre aux caractères autorisés
    },
    email: {
      type: 'string',
      format: 'email',
      maxLength: 254,
    },
    age: {
      type: 'integer',
      minimum: 0,
      maximum: 150,
    },
  },
};

// Logiciel intermédiaire de validation
function validateBody(schema) {
  const validate = ajv.compile(schema);
  return (req, res, next) => {
    if (!validate(req.body)) {
      return res.status(400).json({
        error: 'Validation failed',
        details: validate.errors,
      });
    }
    next();
  };
}

app.post('/api/users', validateBody(createUserSchema), createUser);

Attaques courantes et contre-mesures de validation

AttaqueContre-mesureExemple
Injection SQLRequêtes paramétrées, utilisation d'un ORMdb.query('SELECT * FROM users WHERE id = ?', [id])
Injection NoSQLVérification de type, assainissement des opérateurs $S'assurer que l'entrée est une chaîne de caractères (rejeter les objets)
XSS (via la réponse de l'API)Spécifier le type de contenu (Content-Type), échapper à la sortieContent-Type: application/json
Traversée du cheminSupprimer les séparateurs de chemin de l'entréeNormaliser avec path.basename()
XXE (entité externe XML)Désactiver la résolution des entités externesDésactiver dans les paramètres de l'analyseur XML

🌐 CORS (Cross-Origin Resource Sharing)

CORS est un mécanisme qui contrôle la politique d'origine identique du navigateur. Une mauvaise configuration peut entraîner de graves risques pour la sécurité.

Configuration dangereuse

Access-Control-Allow-Origin : * et Access-Control-Allow-Credentials : true ne peuvent pas être utilisés ensemble. Limitez l'utilisation des caractères génériques aux API publiques uniquement.

Configuration CORS sécurisée

JavaScript (Express)Configuration CORS
const cors = require('cors');

// Spécifier explicitement les origines autorisées
const allowedOrigins = [
  'https://app.example.com',
  'https://admin.example.com',
];

app.use(cors({
  origin(origin, callback) {
    // Autorise la communication de serveur à serveur (pas d'origine)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
  credentials: true,
  maxAge: 86400,  // Cache de prévol : 24 heures
}));

Liste de contrôle CORS

Référence des en-têtes CORS

En-têteObjectifValeur recommandée
Access-Control-Allow-OriginOrigines autoriséesSpécification explicite du domaine
Access-Control-Allow-MethodsMéthodes HTTP autoriséesMinimum requis uniquement
Access-Control-Allow-HeadersEn-têtes de requête autorisésMinimum requis seulement
Access-Control-Allow-CredentialsAutoriser la transmission des cookiestrue (uniquement lorsque l'authentification est requise)
Access-Control-Max-AgeDurée du cache de prévol en secondes86400 (24 heures)
Access-Control-Expose-HeadersEn-têtes de réponse lisibles par JavaScriptSeuls les en-têtes nécessaires tels que RateLimit-*

🤖 AI / LLM API Rate Limiting

LLM APIs introduce new dimensions to rate limiting: token consumption, cost per request, and compute-intensive inference.

Traditional API vs. LLM API Rate Limiting

DimensionTraditional APILLM API
Cost per requestLow, predictableVariable, can be 100x+ (based on tokens)
Rate limit unitRequests per time windowTokens per minute (TPM) + Requests per minute (RPM)
Abuse patternScraping, brute force, DDoSPrompt injection, resource exhaustion, denial-of-wallet
Algorithm fitFixed/Sliding windowToken bucket (weighted by token count)

Token-Aware Rate Limiting (Python)

PythonToken-Based Rate Limiter
import tiktoken
import time
from collections import defaultdict

class TokenRateLimiter:
    """Rate limiter that counts tokens, not just requests."""

    def __init__(self, tokens_per_minute=100_000, requests_per_minute=60):
        self.tpm_limit = tokens_per_minute
        self.rpm_limit = requests_per_minute
        self.usage = defaultdict(lambda: {"tokens": [], "requests": []})
        self.encoder = tiktoken.encoding_for_model("gpt-4")

    def count_tokens(self, text: str) -> int:
        return len(self.encoder.encode(text))

    def check_limit(self, user_id: str, prompt: str) -> dict:
        now = time.time()
        window = now - 60  # 1-minute sliding window
        user = self.usage[user_id]

        # Clean up expired entries
        user["tokens"] = [(t, c) for t, c in user["tokens"] if t > window]
        user["requests"] = [t for t in user["requests"] if t > window]

        # Check RPM
        if len(user["requests"]) >= self.rpm_limit:
            return {"allowed": False, "reason": "RPM limit exceeded"}

        # Check TPM
        token_count = self.count_tokens(prompt)
        used_tokens = sum(c for _, c in user["tokens"])
        if used_tokens + token_count > self.tpm_limit:
            return {"allowed": False, "reason": "TPM limit exceeded"}

        # Record usage
        user["tokens"].append((now, token_count))
        user["requests"].append(now)
        return {"allowed": True, "tokens_used": token_count}
OWASP References

Related: LLM10: Model Denial of Service, ASI04: Cascading Hallucination Attacks

🛡 Prompt Injection & AI Input Validation

Prompt injection is the #1 risk for LLM applications. Apply defense-in-depth with input validation, structural separation, and output verification.

Prompt Sanitization

Strip or escape special tokens, instruction-like patterns, and control characters from user input before including in prompts.

Structural Input Separation

Use delimiters, XML tags, or separate message roles to clearly isolate system instructions from user-provided content.

Output Verification

Validate LLM responses against expected schemas. Check for data leakage, instruction following, and malicious content before rendering.

Prompt Injection Attack Vectors & Mitigations

AttaqueContre-mesureExemple
Direct Prompt InjectionInput sanitization + instruction/data separation"Ignore previous instructions and..."
Indirect Prompt InjectionSanitize RAG results + canary tokensMalicious instructions hidden in retrieved documents
Context StuffingToken limits + input truncationOverloading context window to push out system instructions
Parameter TamperingSchema validation + typed parametersManipulating temperature, max_tokens, or model parameters

Structured Input Validation (Python / Pydantic)

PythonPydantic Validation for LLM Requests
from pydantic import BaseModel, Field, field_validator
import re

class LLMRequest(BaseModel):
    """Validated LLM request with prompt injection defenses."""
    user_message: str = Field(..., max_length=4000)
    max_tokens: int = Field(default=1000, ge=1, le=4096)
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)

    @field_validator("user_message")
    @classmethod
    def sanitize_prompt(cls, v: str) -> str:
        # Block known injection patterns
        patterns = [
            r"(?i)ignore\s+(previous|above|all)\s+(instructions?|prompts?)",
            r"(?i)you\s+are\s+now\s+",
            r"(?i)system\s*:\s*",
            r"(?i)\[INST\]|\[\/INST\]|<\|im_start\|>",
        ]
        for pattern in patterns:
            if re.search(pattern, v):
                raise ValueError("Input contains disallowed patterns")
        return v

# Usage with FastAPI
@app.post("/api/chat")
async def chat(request: LLMRequest):
    # Separate system instructions from user input
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": f"<user_input>{request.user_message}</user_input>"},
    ]
    return await call_llm(messages, request.max_tokens, request.temperature)
OWASP References

Related: LLM01: Prompt Injection, LLM02: Insecure Output Handling, ASI02: Prompt Injection via Tool Results

🔒 Autres en-têtes de sécurité recommandés

HTTP Response HeadersParamètres recommandés
# Désactiver la détection automatique du type de contenu
X-Content-Type-Options: nosniff

# Empêcher l'intégration de l'iframe
X-Frame-Options: DENY

# Enforce HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains

# CSP : Puisque les API ne renvoient que du JSON, bloquer toute exécution de script
Content-Security-Policy: default-src 'none'; frame-ancestors 'none'

# Restreindre les informations sur le référent
Referrer-Policy: no-referrer

# Restreindre les fonctionnalités du navigateur
Permissions-Policy: geolocation=(), camera=(), microphone=()