Aller au contenu principal

Concept #020

Sécurité

Broken Access Control

#0208 min de lecture
  • sécurité
  • web
  • architecture
Broken Access Control : N°1 du TOP 10 OWASP

Le Broken Access Control trône au sommet du TOP 10 OWASP 2021 — et conserve cette première place dans les analyses 2025. Contrairement à l'injection SQL ou au XSS, ce n'est pas un bug de code au sens strict. C'est une erreur de logique : l'application sait qui vous êtes, mais oublie de vérifier ce que vous avez le droit de faire. Vous avez la clé — mais la porte n'aurait pas dû s'ouvrir pour vous.

Qu'est-ce que le Broken Access Control ?

Le contrôle d'accès, c'est l'ensemble des règles qui définissent qui peut faire quoi dans une application. Consulter une facture, modifier un profil, supprimer un compte, accéder au panneau d'administration — chaque action devrait être conditionnée à une vérification explicite des droits de l'utilisateur.

Le Broken Access Control survient quand ces vérifications sont absentes, incomplètes ou contournables. L'utilisateur est authentifié — l'application sait qu'il est connecté — mais elle ne vérifie pas s'il a réellement la permission d'effectuer l'action demandée. Résultat : un utilisateur ordinaire peut lire les données d'un autre, modifier des ressources qui ne lui appartiennent pas, ou accéder à des fonctionnalités réservées aux administrateurs.

Ce qui rend cette vulnérabilité si répandue, c'est qu'elle n'a pas de forme unique. Elle se manifeste dans des dizaines de patterns différents, souvent liés à des décisions d'architecture prises en début de projet et jamais remises en question.

Exemples concrets

IDOR : changer l'ID dans l'URL

L'IDOR (Insecure Direct Object Reference) est la forme la plus classique du Broken Access Control. L'application expose un identifiant de ressource directement dans l'URL ou dans l'API, et se contente de vérifier que l'utilisateur est connecté — sans vérifier qu'il est bien propriétaire de la ressource.

Scénario typique : Alice consulte sa facture. L'URL est :

GET /api/invoices/1042

Elle remarque l'identifiant 1042. Par curiosité, elle change ce chiffre :

GET /api/invoices/1041

Si le serveur répond avec la facture de Bob, l'application est vulnérable. Il n'y a eu aucune vérification que la facture 1041 appartient bien à Alice. Un attaquant peut itérer sur les identifiants et extraire les données de tous les utilisateurs — automatiquement, en quelques secondes.

// Endpoint vulnérable — vérifie l'authentification, pas l'autorisation
app.get("/api/invoices/:id", authenticate, async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id);
 
  if (!invoice) return res.status(404).json({ error: "Not found" });
 
  // BUG : on retourne la facture sans vérifier qu'elle appartient à req.user
  return res.json(invoice);
});

Élévation de privilèges

L'élévation de privilèges (privilege escalation) est une autre variante : un utilisateur accède à des fonctionnalités réservées à un rôle supérieur au sien.

Exemple horizontal : Alice accède au compte de Bob, un autre utilisateur avec le même niveau de droits. Elle modifie le profil de Bob en changeant l'identifiant dans la requête PATCH /api/users/57 alors que son propre ID est 58.

Exemple vertical : Alice, utilisatrice ordinaire, envoie une requête à un endpoint d'administration — GET /api/admin/users — qui n'a jamais été protégé car les développeurs pensaient que "personne ne connaîtrait l'URL". C'est de la sécurité par l'obscurité, pas du contrôle d'accès.

// Endpoint d'administration sans vérification de rôle
app.get("/api/admin/users", authenticate, async (req, res) => {
  // authenticate vérifie que l'utilisateur est connecté
  // mais rien ne vérifie que req.user.role === "admin"
  const users = await db.users.findAll();
  return res.json(users);
});

Pourquoi c'est N°1 du TOP 10 OWASP

L'OWASP a placé le Broken Access Control en tête pour une raison simple : c'est la catégorie de vulnérabilités la plus fréquemment rencontrée lors des audits et des tests de pénétration. Dans l'édition 2021, 94 % des applications testées présentaient une forme ou une autre de contrôle d'accès défaillant.

Contrairement aux injections qui peuvent être bloquées par des mécanismes automatiques (requêtes paramétrées, échappement), le contrôle d'accès est intrinsèquement lié à la logique métier de chaque application. Il n'existe pas de librairie qui sache automatiquement qu'un utilisateur ne devrait pas lire la facture d'un autre — c'est au développeur de l'exprimer explicitement.

C'est aussi une vulnérabilité difficile à détecter avec des scanners automatiques. Un scanner peut trouver une injection SQL en envoyant des payloads connus. Il ne peut pas toujours deviner qu'un utilisateur de rôle "user" ne devrait pas pouvoir appeler /api/admin/export.

Comment se protéger

Vérification côté serveur, toujours

La règle absolue : toute vérification d'accès doit se faire côté serveur. Masquer un bouton dans l'interface, griser un champ, désactiver un lien côté client — aucune de ces mesures ne constitue un contrôle d'accès. Un attaquant envoie ses requêtes directement à l'API, sans passer par votre interface.

Chaque requête qui touche une ressource doit vérifier deux choses :

  1. L'utilisateur est-il authentifié ? (qui est-il ?)
  2. L'utilisateur a-t-il le droit d'effectuer cette action sur cette ressource spécifique ? (qu'a-t-il le droit de faire ?)

Principe du moindre privilège

Un utilisateur ne devrait avoir accès qu'à ce dont il a strictement besoin pour accomplir son travail. Par défaut, tout accès est refusé — il faut l'accorder explicitement. Ce principe s'applique aussi bien aux rôles utilisateurs qu'aux services internes, aux comptes de base de données, et aux tokens d'API.

RBAC : Role-Based Access Control

Le RBAC est le modèle le plus répandu pour structurer les permissions. Les utilisateurs se voient attribuer des rôles (admin, editor, viewer...), et les rôles définissent les permissions (read:invoices, write:users, delete:comments...). Une action n'est autorisée que si le rôle de l'utilisateur dispose de la permission correspondante.

// Définition des permissions par rôle
const rolePermissions: Record<string, string[]> = {
  admin: ["read:users", "write:users", "delete:users", "read:invoices"],
  editor: ["read:invoices", "write:invoices"],
  viewer: ["read:invoices"],
};
 
function hasPermission(role: string, permission: string): boolean {
  return rolePermissions[role]?.includes(permission) ?? false;
}

Middleware de vérification d'accès en TypeScript/Express

Voici une implémentation concrète d'un middleware qui combine vérification de rôle et vérification de propriété de ressource :

import { Request, Response, NextFunction } from "express";
 
// Middleware de vérification de rôle
function requireRole(...allowedRoles: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const userRole = req.user?.role;
 
    if (!userRole || !allowedRoles.includes(userRole)) {
      return res.status(403).json({ error: "Accès refusé : rôle insuffisant" });
    }
 
    next();
  };
}
 
// Middleware de vérification de propriété (ownership)
function requireOwnership(
  getResourceOwnerId: (req: Request) => Promise<string | null>
) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // Les admins peuvent accéder à toutes les ressources
    if (req.user?.role === "admin") return next();
 
    const ownerId = await getResourceOwnerId(req);
 
    if (ownerId === null) {
      return res.status(404).json({ error: "Ressource introuvable" });
    }
 
    if (ownerId !== req.user?.id) {
      return res.status(403).json({ error: "Accès refusé : ressource non autorisée" });
    }
 
    next();
  };
}
 
// Utilisation sur les routes
const getInvoiceOwner = async (req: Request) => {
  const invoice = await db.invoices.findById(req.params.id);
  return invoice?.userId ?? null;
};
 
// Factures : accessible par l'utilisateur propriétaire ou un admin
app.get(
  "/api/invoices/:id",
  authenticate,
  requireOwnership(getInvoiceOwner),
  async (req, res) => {
    const invoice = await db.invoices.findById(req.params.id);
    return res.json(invoice);
  }
);
 
// Panneau admin : accessible uniquement par les admins
app.get(
  "/api/admin/users",
  authenticate,
  requireRole("admin"),
  async (req, res) => {
    const users = await db.users.findAll();
    return res.json(users);
  }
);

Bonnes pratiques

Refuser par défaut. La règle d'or : tout accès est interdit jusqu'à preuve du contraire. Si une route n'a pas de middleware de contrôle d'accès, elle est accessible à tous — traitez chaque route non protégée comme une faille potentielle.

Ne pas exposer les IDs séquentiels. Si vos identifiants de ressources sont des entiers auto-incrémentés (1, 2, 3...), ils facilitent les attaques IDOR par énumération. Préférez les UUIDs — ils ne sont pas une protection en soi, mais rendent l'énumération impraticable.

Logger les refus d'accès. Un utilisateur qui déclenche des centaines d'erreurs 403 en quelques minutes est probablement en train de sonder vos endpoints. Journalisez les accès refusés et mettez en place des alertes.

Tester explicitement les permissions. Écrivez des tests automatisés qui vérifient qu'un utilisateur de rôle "viewer" ne peut pas appeler les routes d'administration, et qu'un utilisateur ne peut pas accéder aux données d'un autre. Ces cas ne sont généralement pas couverts par les tests fonctionnels classiques.

Centraliser la logique d'autorisation. Évitez de disperser les vérifications de droits dans chaque handler. Un middleware dédié, une librairie d'autorisation (casl, accesscontrol, oso) ou une couche de service centralisée réduisent le risque d'oublier une vérification sur un endpoint.

Le récapitulatif

VulnérabilitéVecteurExemple
IDORIdentifiant exposé dans l'URL/api/invoices/10421041
Escalade horizontaleAccès aux données d'un pairModifier le profil d'un autre utilisateur
Escalade verticaleAccès à un rôle supérieurUtilisateur ordinaire → endpoint admin
Bypass via clientLogique de sécurité côté frontMasquer un bouton sans protéger l'API

La règle d'or : authentification et autorisation sont deux problèmes distincts. Savoir qui est l'utilisateur ne suffit pas — il faut vérifier, pour chaque action sur chaque ressource, s'il a explicitement le droit de l'effectuer. Cette vérification doit se faire côté serveur, à chaque requête, sans exception.