Featured image of post Décider de l'autorité par ce que vous détenez, non par qui vous êtes : l'architecture de sécurité basée sur les capacités

Décider de l'autorité par ce que vous détenez, non par qui vous êtes : l'architecture de sécurité basée sur les capacités

Un traité architectural rigoureux sur la sécurité basée sur les capacités (Capability-Based Security) : découpage par colonnes (ACL) vs par lignes (Capacités) de la matrice de contrôle d'accès de Butler Lampson, le problème du député confus de Norm Hardy (1988), le modèle Object-Capability (OCap) de Mark S. Miller, l'atténuation par facettes, la révocation via le Caretaker de Redell et les Membranes, ainsi que les déploiements modernes dans seL4, FreeBSD Capsicum, CHERI/Morello et WASI.

À travers les systèmes d’exploitation modernes, les infrastructures cloud et les applications web, les brèches de sécurité catastrophiques suivent un schéma étonnamment répétitif. La falsification de requêtes intersites (CSRF), la falsification de requêtes côté serveur (SSRF), les injections SQL et de commandes, les évasions de conteneurs et les attaques de la chaîne d’approvisionnement logicielle dans les écosystèmes npm ou PyPI semblent appartenir à des couches totalement différentes. Pourtant, sous leurs détails de surface se cache un seul et même défaut architectural.

Ce défaut est notre dépendance, vieille d’un demi-siècle, aux listes de contrôle d’accès (Access Control Lists, ACL) couplées à l’autorité ambiante (Ambient Authority) — le paradigme selon lequel les décisions d’accès dépendent de qui vous êtes (l’identité du sujet) plutôt que de ce que vous détenez, et dans lequel la désignation (nommer une ressource via un chemin de chaîne ou une URL falsifiable) est séparée de l’autorité (la permission ambiante portée par l’appelant).

Dans cet article, nous examinons systématiquement le paradigme alternatif qui élimine cette vulnérabilité structurelle à la racine : la sécurité basée sur les capacités (Capability-Based Security). En partant de la décomposition mathématique de la matrice de contrôle d’accès $M[S, O]$ de Butler Lampson (1971), nous retraçons le classique problème du député confus (Confused Deputy Problem) de Norm Hardy (1988), formulons le modèle Object-Capability (OCap) de Mark S. Miller et le principe de moindre autorité (POLA), construisons des patrons de code concrets pour l’atténuation (facettes) et la révocation immédiate ou transitive (Caretaker de Redell et Membranes), et passons en revue les déploiements industriels modernes incluant le micronoyau formellement vérifié seL4, FreeBSD Capsicum, les pointeurs matériels CHERI / ARM Morello et WebAssembly WASI.


1. Deux paradigmes d’autorité : badges d’identité vs clés de voiturier (Valet Key)

Avant de plonger dans la théorie formelle des systèmes d’exploitation, comparons deux manières d’appliquer la sécurité physique dans le monde réel.

Paradigme A : Badges d’identité et registres de portes (Le modèle ACL)

Imaginez un immeuble d’entreprise de haute sécurité où chaque porte est gardée par un agent tenant un registre contenant la liste des employés autorisés à entrer dans la salle 302 (une liste de contrôle d’accès). Lorsque vous demandez à entrer, le garde inspecte votre badge d’identité (Identité / uid) et vérifie si votre nom figure sur le registre.

Supposons maintenant que vous souhaitiez qu’un stagiaire (un programme utilitaire ou une bibliothèque tierce) récupère un seul fichier, Q3_Budget.pdf, dans la salle 302. Le stagiaire n’étant pas sur la liste, vous devez lui prêter votre propre badge d’identité (comme setuid sous UNIX ou l’exécution de code sous votre propre identité). Dès l’instant où le stagiaire entre dans la salle 302 avec votre badge, il acquiert automatiquement une autorité ambiante (Ambient Authority) sur absolument tout ce qui se trouve dans cette pièce, y compris Payroll_Master.db. Pire encore, si un attaquant externe échange la note du stagiaire pour lui faire récupérer Payroll_Master.db, le garde le laissera faire car le stagiaire porte votre badge !

Paradigme B : La clé de voiturier / Valet Key (Le modèle de capacité)

Comparez cela au moment où vous confiez votre voiture à un voiturier d’hôtel. Vous n’appelez pas la préfecture pour inscrire le permis du voiturier sur une liste attachée à votre véhicule, et vous ne lui donnez pas vos papiers d’identité. Vous lui remettez une clé de voiturier (Valet Key), qui incarne trois propriétés architecturales fondamentales :

  1. Unification de la désignation et de l’autorité : La clé désigne simultanément quel véhicule spécifique doit être opéré et autorise le détenteur à démarrer le moteur. La voiture ignore qui tient la clé ; la possession du jeton infalsifiable est une preuve nécessaire et suffisante d’autorité.
  2. Atténuation (Moindre privilège) : Contrairement à votre clé maîtresse, la clé de voiturier ne peut ouvrir ni le coffre ni la boîte à gants, et limite parfois la vitesse maximale. Elle n’accorde que l’autorité minimale exacte requise pour garer la voiture.
  3. Délégation décentralisée : Si le premier voiturier passe le relais à un collègue, il lui remet simplement la clé sans modifier de base de données centrale.

En informatique, le paradigme A correspond au modèle ACL, tandis que le paradigme B est le modèle de sécurité basée sur les capacités (Capabilities).


2. La matrice de contrôle d’accès $M[S, O]$ de Butler Lampson et ses deux décompositions

En 1971, le lauréat du prix Turing Butler Lampson a publié “Protection”, introduisant la matrice de contrôle d’accès comme abstraction mathématique universelle des mécanismes de protection. L’état de protection d’un système est défini par un triplet $(S, O, M)$ :

  • Un ensemble de sujets $S = \{s_1, s_2, \dots, s_m\}$ : Entités actives capables d’initier des opérations (utilisateurs, processus, threads, modules logiciels).
  • Un ensemble d’objets $O = \{o_1, o_2, \dots, o_n\}$ : Ressources protégées (fichiers, segments mémoire, sockets IPC, périphériques), avec généralement $S \subseteq O$.
  • Un ensemble universel de droits $\mathcal{R} = \{\mathsf{read}, \mathsf{write}, \mathsf{execute}, \mathsf{append}, \mathsf{grant}, \mathsf{own}\}$ : Les opérations discrètes pouvant être autorisées sur un objet.
  • Une matrice de contrôle d’accès $M : S \times O \to 2^{\mathcal{R}}$ : Chaque entrée $M[s, o] \subseteq \mathcal{R}$ spécifie l’ensemble exact des opérations que le sujet $s \in S$ est autorisé à effectuer sur l’objet $o \in O$.

Considérons par exemple trois sujets $S = \{\text{Alice}, \text{Compiler}, \text{Logger}\}$ et trois fichiers $O = \{\texttt{src.f}, \texttt{/SYSX/STAT}, \texttt{/SYSX/BILL}\}$ :

$$ M[S, O] = \begin{array}{c|ccc} & \texttt{src.f} & \texttt{/SYSX/STAT} & \texttt{/SYSX/BILL} \\ \hline \text{Alice} & \{\mathsf{read}, \mathsf{write}\} & \emptyset & \emptyset \\ \text{Compiler} & \{\mathsf{read}\} & \{\mathsf{append}\} & \{\mathsf{read}, \mathsf{write}\} \\ \text{Logger} & \emptyset & \{\mathsf{read}, \mathsf{write}\} & \emptyset \end{array} $$

Comme $M[S, O]$ est extrêmement creuse dans tout système réel, chaque architecte doit la décomposer selon l’un de ses deux axes : découper par colonnes (listes attachées aux objets), ou découper par lignes (jetons détenus par les sujets).

  flowchart TD
    ACM["Matrice de contrôle d'accès de Butler Lampson M[S, O]<br/>Lignes : Sujets S / Colonnes : Objets O"]
    ACM -->|"Découpage par colonnes (Par objet)"| ACL["Listes de contrôle d'accès (ACL)<br/>Chaque objet o stocke les paires (Sujet s, Droits R)"]
    ACM -->|"Découpage par lignes (Par sujet)"| CAP["Capacités (C-Lists / OCap)<br/>Chaque sujet s détient des descripteurs infalsifiables Cap(o, R)"]

    ACL --> ACL_PROP["1. Sépare la désignation ('/etc/passwd') de l'autorité (uid ambiant)<br/>2. Le sujet porte TOUS ses privilèges simultanément (Autorité ambiante)<br/>3. Structurellement vulnérable au député confus (CSRF / SSRF)"]
    CAP --> CAP_PROP["1. Unifie la désignation (objet o) et l'autorité (droits R) en un seul descripteur<br/>2. Pas de désignation sans autorité (isolation complète des espaces de noms)<br/>3. Applique le principe de moindre privilège (POLA) à grain fin"]

2.1 Découpage par colonnes : Listes de contrôle d’accès (ACL)

En découpant verticalement la matrice $M[S, O]$ par colonnes, on associe à chaque objet $o \in O$ la liste des entrées non vides de sa colonne :

$$ \text{ACL}(o) = \bigl\{ (s, M[s, o]) \;\big|\; s \in S,\; M[s, o] \neq \emptyset \bigr\} $$

Les permissions UNIX/POSIX, les DACL Windows NTFS et les politiques IAM cloud suivent ce modèle, ce qui impose la séparation entre la désignation et l’autorité. Un programme désigne un objet via une chaîne publique falsifiable comme "/SYSX/BILL", et le moniteur de référence du noyau vérifie l’identité ambiante (uid) du processus appelant. Le processus exerce donc en permanence l’union de tous les droits de sa ligne $\bigcup_{o \in O} M[s, o]$ à chaque appel système : c’est l’autorité ambiante (Ambient Authority).

2.2 Découpage par lignes : Capacités (Dennis & Van Horn 1966)

En 1966, Jack B. Dennis et Earl C. Van Horn ont introduit la décomposition duale : découper horizontalement $M[S, O]$ par lignes. À chaque sujet $s \in S$, on associe un espace de capacités (CSpace ou C-List) :

$$ \text{CSpace}(s) = \bigl\{ \text{Cap}(o, R) \;\big|\; o \in O,\; R = M[s, o] \neq \emptyset \bigr\} $$

Chaque élément $c = \text{Cap}(o, R)$ est une capacité (Capability) — un jeton d’autorité infalsifiable qui lie la désignation (un pointeur ou descripteur direct vers l’objet $o$) et l’autorité (le sous-ensemble de droits permis $R \subseteq \mathcal{R}$) en une référence indivisible. Sans capacité sur $o$, un sujet $s$ ne peut même pas nommer ou adresser $o$ : Pas de désignation sans autorité ; pas d’autorité sans désignation.

Dimension architecturaleListes de contrôle d’accès (ACL — Colonnes)Capacités (OCap / C-Lists — Lignes)
Décomposition de $M[S, O]$Stockée par colonne sur chaque objet $o \in O$Stockée par ligne détenue par chaque sujet $s \in S$
Philosophie d’autorisationQui vous êtes (Who You Are) : Identité / uid de l’appelantCe que vous détenez (What You Hold) : Le jeton de capacité présenté
Désignation vs AutoritéSéparées (Chemin de chaîne falsifiable + Identité ambiante)Unifiées (Le descripteur infalsifiable nomme $o$ et accorde $R$)
Modèle d’autoritéAutorité ambiante : Tous les droits de uid sont actifs à chaque appelAutorité sélective : Seul le descripteur explicitement passé est actif
Vulnérabilité au député confusStructurellement vulnérable (CSRF, SSRF, escalade setuid)Structurellement immunisé (Le client doit passer un descripteur autorisé)
Systèmes représentatifsACL UNIX/POSIX, Windows NTFS DACL, AWS IAMKeyKOS, seL4, FreeBSD Capsicum, Fuchsia Zircon, CHERI, WASI

3. Le problème du député confus (Confused Deputy) : le classique de Norm Hardy (1988)

Pourquoi séparer la désignation de l’autorité est-il si dangereux ? L’architecte informatique Norm Hardy en a donné l’explication définitive en octobre 1988 dans ACM SIGOPS Operating Systems Review sous le titre “The Confused Deputy (or why capabilities might have been invented)", relatant un désastre réel survenu au milieu des années 1970 sur le système d’exploitation commercial Tymshare.

3.1 Anatomie de la catastrophe du compilateur /SYSX/FORT

Sur le système Tymshare, le compilateur Fortran résidait dans /SYSX/FORT :

  1. Exigence de statistiques : Les administrateurs souhaitaient que /SYSX/FORT ajoute un enregistrement statistique au fichier /SYSX/STAT à chaque compilation.
  2. Octroi d’une autorité ambiante : Pour cela, l’exécutable /SYSX/FORT s’est vu accorder une autorité d’écriture ambiante sur tout le répertoire /SYSX (analogue au setuid UNIX).
  3. Nom de fichier de sortie fourni par l’utilisateur : Le compilateur acceptait en argument une chaîne de caractères indiquant où écrire la sortie de débogage (ex. FORT(SRC="prog.f", OUT="listing.txt")).
  4. Les joyaux de la couronne dans /SYSX/BILL : Le même répertoire /SYSX abritait la base de données principale de facturation client, /SYSX/BILL.

Un jour, un utilisateur astucieux a invoqué /SYSX/FORT en passant la chaîne "/SYSX/BILL" comme fichier de sortie. Lorsque le compilateur a exécuté open("/SYSX/BILL", WRITE), le moniteur de référence ACL de l’OS a demandé : « Le processus actif (/SYSX/FORT) a-t-il la permission d’écrire dans /SYSX ? » La réponse étant Oui, le noyau a autorisé l’ouverture, et le compilateur a consciencieusement écrasé la base de données de facturation /SYSX/BILL de l’entreprise avec des diagnostics de compilation Fortran !

  flowchart TD
    subgraph ACL_WORLD["Modèle ACL + Autorité ambiante (Le député confus)"]
        U1["Utilisateur malveillant<br/>(Aucun accès à /SYSX/BILL)"] -->|"1. Passe la chaîne OUT='/SYSX/BILL'"| D1["Député : Compilateur /SYSX/FORT<br/>(Autorité ambiante : Écriture sur /SYSX/*)"]
        D1 -->|"2. open('/SYSX/BILL', WRITE)<br/>Exerce implicitement sa propre autorité ambiante"| OS1["Moniteur de référence OS (Vérification ACL)"]
        OS1 -->|"3. Le compilateur a le droit d'écriture sur /SYSX -> AUTORISÉ !"| BILL1["Base de facturation /SYSX/BILL<br/>[CORROMPUE / ÉCRASÉE !]"]
    end

    subgraph CAP_WORLD["Modèle de capacités (Désignation et autorité unifiées)"]
        U2["Utilisateur<br/>(Ne détient des capacités que sur ses fichiers)" ] -->|"1. Passe la capacité de sortie cap_out<br/>(Impossible de passer une capacité sur /SYSX/BILL !)"| D2["Député : Compilateur /SYSX/FORT<br/>(Doté UNIQUEMENT de cap_stat)"]
        D2 -->|"2a. write(cap_out, debug_output)<br/>Utilise l'autorité explicitement fournie par l'utilisateur"| OUT2["Fichier de sortie listing.txt"]
        D2 -->|"2b. append(cap_stat, usage_stats)<br/>Utilise la capacité privée confiée à l'installation"| STAT2["Fichier statistique /SYSX/STAT"]
    end

3.2 Pourquoi le compilateur était-il « confus » ? Et ses descendants modernes

Hardy a qualifié /SYSX/FORT de député confus (Confused Deputy) car il agissait au service de deux mandants distincts (l’administrateur système et l’utilisateur) sans pouvoir indiquer au noyau quelle autorité appliquer à quel chemin. Dans une architecture basée sur les capacités, le compilateur ne reçoit que la capacité privée $c_{\text{stat}} = \text{Cap}(\texttt{/SYSX/STAT}, \{\mathsf{append}\})$, et l’utilisateur doit lui transmettre directement une capacité d’écriture déjà ouverte $c_{\text{out}} = \text{Cap}(f_{\text{out}}, \{\mathsf{write}\})$, rendant l’attaque impossible.

Les classes de vulnérabilités modernes les plus dévastatrices sont toutes des députés confus :

  • Cross-Site Request Forgery (CSRF) : Le navigateur web est un député confus qui attache automatiquement votre autorité ambiante (le cookie de session bank.example) à une requête forgée par evil.example. Les jetons CSRF corrigent cela en exigeant que le formulaire présente un jeton de capacité infalsifiable.
  • Server-Side Request Forgery (SSRF) : Le serveur web récupère une URL fournie par l’utilisateur ("http://169.254.169.254/latest/meta-data/") en utilisant sa propre autorité réseau ambiante au sein du VPC cloud.
  • Attaques de la chaîne d’approvisionnement (npm / PyPI) : Une minuscule bibliothèque tierce peut lire ~/.ssh/id_rsa parce que sous les ACL POSIX, chaque fonction exécutée dans votre processus hérite de toute l’autorité ambiante de votre uid.

4. Le modèle Object-Capability (OCap), l’atténuation et la révocation

Dans sa thèse de doctorat de 2006 à l’Université Johns Hopkins (“Robust Composition”), Mark S. Miller a démontré que les langages orientés objet à mémoire sûre possèdent déjà le mécanisme exact requis pour appliquer le principe de moindre privilège (POLA) jusqu’au niveau de chaque objet : la référence d’objet elle-même.

4.1 Trois invariants OCap et les trois lois d’acquisition des capacités

Pour qu’un langage à mémoire sûre (comme Hardened JavaScript / SES, E ou les composants Wasm) devienne un système Object-Capability (OCap), l’environnement d’exécution doit garantir trois invariants :

  1. Pas de références forgées : Impossible de convertir un entier en pointeur d’objet ou d’utiliser l’arithmétique des pointeurs hors limites.
  2. Aucun état global mutable (Zéro autorité ambiante) : La portée globale ne doit contenir ni variables mutables ni modules d’E/S ambiants (fs, net), et les prototypes primordiaux (Object.prototype) doivent être gelés en profondeur (Object.freeze).
  3. Encapsulation absolue : L’état lexical privé d’une fermeture (closure) ou les champs #private ne peuvent être inspectés de l’extérieur.

Dans le graphe de références $G = (V, E)$, une nouvelle arête $(A \to C)$ ne peut naître que de trois façons : 1. Parentalité (Création, new C()), 2. Dotation (Endowment lors de la création d’une fermeture), ou 3. Introduction (un introducteur $B$ détenant $(B \to A)$ et $(B \to C)$ passe $C$ en argument à $A$) :

$$ \text{Loi d'introduction (Law of Introduction)} : \quad (B \to A) \;\land\; (B \to C) \;\;\Longrightarrow\;\; B \text{ peut établir } (A \to C) $$

4.2 Atténuation par facettes et révocation via le Caretaker de Redell et les Membranes

Contrairement au mythe répandu, les capacités permettent une atténuation et une révocation instantanées, résolues dès 1974 par David D. Redell avec le patron Caretaker (Gardien) et généralisées par les Membranes révocables de Mark S. Miller :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. Facette d'atténuation (Attenuating Facet) : lecture seule limitée à un préfixe
function makeScopedReadFacet(fullStorageCap, allowedPrefix) {
  return Object.freeze({
    read(key) {
      if (typeof key !== "string" || !key.startsWith(allowedPrefix)) {
        throw new Error(`Accès refusé : la clé doit commencer par '${allowedPrefix}'`);
      }
      return fullStorageCap.read(key);
    }
    // write() et deleteAll() sont totalement absents de la facette !
  });
}

// 2. Patron Caretaker de Redell (1974) pour la révocation instantanée en O(1)
function makeCaretaker(targetCap) {
  let currentTarget = targetCap; // Pointeur privé mutable dans la fermeture
  const caretaker = Object.freeze({
    read(...args) {
      if (currentTarget === null) throw new Error("RevokedCapabilityError : Accès révoqué");
      return currentTarget.read(...args);
    },
    write(...args) {
      if (currentTarget === null) throw new Error("RevokedCapabilityError : Accès révoqué");
      return currentTarget.write(...args);
    }
  });
  const revoker = Object.freeze({
    revoke() { currentTarget = null; } // Coupe le lien et permet le ramasse-miettes (GC)
  });
  return Object.freeze({ caretaker, revoker });
}

Lorsqu’une capacité renvoie d’autres capacités (comme un répertoire renvoyant un sous-répertoire), une Membrane révocable utilise Proxy et WeakMap pour envelopper récursivement toutes les valeurs de retour et arguments traversant la frontière, désactivant instantanément tout le sous-graphe lors de l’appel à revoke().


5. Déploiements industriels : seL4, FreeBSD Capsicum, CHERI et WebAssembly WASI

Évitons d’abord un piège terminologique : les capabilities(7) de Linux (CAP_NET_ADMIN, etc.) ne sont PAS des capacités d’objet ! Elles découpent simplement le privilège ambiant de root en une quarantaine de drapeaux par thread sans désigner d’objet précis. Les véritables architectures de capacités industrielles s’illustrent dans quatre systèmes :

  1. Le micronoyau formellement vérifié seL4 : Premier noyau d’OS doté d’une preuve mathématique complète vérifiée par machine (sous Isabelle/HOL), seL4 n’offre aucune autorité ambiante. Toutes les ressources du noyau (threads, espaces d’adressage, points de terminaison IPC, cadres mémoire) sont manipulées exclusivement via des capacités stockées dans le CSpace (tables CNode) d’un thread, avec atténuation par seL4_CNode_Mint et révocation transitive via l’arbre de dérivation CDT (seL4_CNode_Revoke).
  2. FreeBSD Capsicum : Créé à l’Université de Cambridge et chez Google, Capsicum transforme les descripteurs de fichiers UNIX (fd) en véritables capacités. Dès qu’un processus appelle cap_enter(), le noyau bloque irréversiblement tout appel système accédant à un espace de noms global (open(), connect(), bind() renvoient ECAPMODE). Le processus ne peut agir que sur les descripteurs qu’il détient déjà (et leurs sous-chemins relatifs via openat(dir_fd, ...) sans remontée ".."), tout en restreignant finement plus de 60 bits de droits via cap_rights_limit(fd, &rights).
  3. Capacités dans le silicium : CHERI et ARM Morello : Développé par SRI International et l’Université de Cambridge et matérialisé par ARM dans le processeur Morello ainsi que dans CHERIoT-RISC-V, CHERI remplace les pointeurs 64 bits par des capacités matérielles de 128 bits accompagnées d’un bit de validité hors bande (Tag Bit) :
$$ \underbrace{\text{Tag (1 bit)}}_{\text{Bit de provenance matériel}} \;\Big|\; \underbrace{\text{Permissions (18 bits)}}_{\text{Load, Store, Exec, Cap-RW}} \;\Big|\; \underbrace{\text{Object Type (18 bits)}}_{\text{Scellement matériel}} \;\Big|\; \underbrace{\text{Compressed Bounds (28 bits)}}_{\text{Bornes compressées [Base, Top)}} \;\Big|\; \underbrace{\text{Address Cursor (64 bits)}}_{\text{Adresse virtuelle 64 bits}} $$

À chaque accès mémoire, le processeur vérifie en un seul cycle que le curseur se situe dans [Base, Top) et respecte les permissions. Toute écriture d’entier écrasant un octet d’une capacité en RAM efface automatiquement le bit Tag à 0, rendant le pointeur forgé non déréférençable et éliminant matériellement les dépassements de tampon et attaques ROP. 4. WebAssembly (WASI) et les Macaroons : Les modules WASI s’exécutent sans autorité ambiante et ne peuvent accéder qu’aux descripteurs de répertoires pré-ouverts injectés explicitement par l’hôte. Dans le cloud distribué, les Macaroons de Google utilisent des chaînes de signatures HMAC imbriquées permettant à tout détenteur d’atténuer localement un jeton en y ajoutant des restrictions contextuelles (Caveats) sans consulter de serveur central.

comments powered by Disqus