Aller au contenu

Signature XML et enveloppement : pourquoi une assertion SAML valide peut mentir

Signature XML et enveloppement : pourquoi une assertion SAML valide peut mentir

Section intitulée « Signature XML et enveloppement : pourquoi une assertion SAML valide peut mentir »

Une bibliothèque de vérification de signature répond « valide ». L’application en conclut que le document qu’elle lit est authentique, et accorde l’accès. Ces deux phrases ne parlent pas du même objet, et c’est tout le problème : la bibliothèque a vérifié un fragment désigné par une référence, l’application lit un fragment désigné par sa position. Quand un attaquant parvient à ce que ces deux fragments diffèrent, la signature reste parfaitement valable et l’assertion exploitée est la sienne. C’est la classe d’attaques dite d’enveloppement de signature XML, et elle a frappé la plupart des implémentations SAML.

→ Lire l’explication complète de la marmotte

La signature XML, telle que le W3C l’a définie, n’est pas une signature sur un fichier. C’est une structure qui contient une ou plusieurs références, chacune désignant les données couvertes, accompagnée du condensé de ces données et d’une signature sur l’ensemble des références. La spécification SAML 2.0 Core de l’OASIS, publiée le 15 mars 2005, ne le cache pas : « la spécification XML Signature définit une syntaxe XML générale pour signer des données, avec de la flexibilité et beaucoup de choix », et sa section 5.4 existe précisément pour « détailler des contraintes sur ces facilités, de sorte que les processeurs SAML n’aient pas à traiter la pleine généralité du traitement de la signature XML ».

Les contraintes que SAML impose sont de bon sens, et il vaut la peine de voir ce qu’elles verrouillent. XML Signature « a trois manières de relier une signature à un document : englobante, enveloppée et détachée », et SAML tranche : les assertions et les messages de protocole « DOIVENT utiliser des signatures enveloppées ». Ensuite, sur les références : les assertions et messages « DOIVENT fournir une valeur pour l’attribut ID de l’élément racine de l’assertion ou du message signé », et « les signatures DOIVENT contenir une unique ds:Reference contenant une référence au même document vers la valeur de l’attribut ID de l’élément racine » ; si cette valeur est foo, alors « l’attribut URI de l’élément ds:Reference DOIT être #foo ». Une seule référence, vers un identifiant, dans le même document.

Troisième contrainte, et elle est capitale : la spécification exige que les identifiants soient uniques, avec une rigueur mathématique inhabituelle. Toute partie qui attribue un identifiant « DOIT s’assurer qu’il existe une probabilité négligeable » de collision, la probabilité de deux identifiants identiques devant être « inférieure ou égale à 2⁻¹²⁸ et DEVRAIT être inférieure ou égale à 2⁻¹⁶⁰ ». Et surtout : « là où un objet de données déclare qu’il a un identifiant particulier, il DOIT y avoir exactement une telle déclaration ». Cette dernière phrase est la défense contre l’attaque décrite ici, et c’est aussi celle que les implémentations ont le plus souvent oubliée.

Comparaison de deux documents de réponse SAML. À gauche, le document légitime : une signature dont la référence pointe vers l'identifiant A1, et l'assertion portant cet identifiant A1, avec le sujet alice, couverte par la signature ; le vérificateur valide A1, l'application lit A1, c'est le même élément et l'accès est correct. À droite, le document manipulé : la signature est inchangée et sa référence pointe toujours vers A1, mais l'assertion A1 a été déplacée telle quelle dans un conteneur que l'application ne consulte pas, et une assertion forgée portant l'identifiant A2, non signée, avec le sujet admin, a été placée à l'endroit où l'application regarde ; la signature reste valable et l'accès est usurpé. Un bandeau conclut que la faute n'est pas dans la cryptographie : le vérificateur résout une référence par identifiant tandis que l'application lit une position dans l'arbre, et la parade consiste à n'exploiter que l'élément que la signature a effectivement couvert.

La manipulation se décrit en trois gestes, et aucun ne touche à la cryptographie. L’attaquant part d’une réponse légitime, obtenue pour son propre compte ou interceptée. Il déplace l’assertion signée vers un emplacement du document que l’application ne consulte pas : un élément d’extension, un conteneur inutilisé, une partie du schéma prévue pour l’extensibilité. L’assertion y reste intacte, octet pour octet, ce qui est la condition pour que son condensé continue de correspondre. Il insère ensuite, à l’emplacement où l’application va chercher l’assertion, une assertion de son choix, non signée, portant le sujet, les attributs et les conditions qu’il souhaite. Il laisse la signature inchangée.

À la vérification, la bibliothèque résout la référence #A1, trouve l’élément correspondant, quelque part dans le document, recalcule son condensé, le compare à celui de la signature, vérifie la signature sur les références avec la clé publique du fournisseur d’identité, et répond « valide ». Tout est exact. L’application, de son côté, prend la première assertion sous la réponse, ou l’assertion que son analyseur lui renvoie pour un chemin donné, et y trouve le contenu forgé.

Deux variantes aggravent le tableau. La première exploite une divergence d’analyse : si deux éléments portent le même identifiant, parce que l’attaquant a dupliqué A1, le vérificateur et l’analyseur applicatif peuvent résoudre cet identifiant différemment, le premier retenant par exemple la première occurrence et le second la dernière. C’est exactement ce que l’exigence d’unicité citée plus haut interdit, et c’est pourquoi un vérificateur doit refuser un document où un identifiant apparaît deux fois plutôt que de choisir. La seconde exploite l’héritage de signature, dont la spécification autorise explicitement l’usage : « quand une assertion SAML ne contient pas d’élément ds:Signature mais est contenue dans un élément SAML englobant qui en contient un, et que la signature s’applique à l’élément Assertion et à tous ses enfants, alors l’assertion peut être considérée comme héritant de la signature de l’élément englobant ». La condition est « et à tous ses enfants » ; une implémentation qui conclut à l’héritage sans vérifier que la signature de la réponse couvre réellement l’assertion lue accepte n’importe quoi. La spécification met d’ailleurs en garde sur cet héritage : les signatures héritées « devraient être utilisées avec soin quand l’objet contenu est destiné à avoir une durée de vie non transitoire », parce qu’« il faut conserver le contexte entier pour permettre la validation ».

Ce défaut n’est pas propre à SAML : il appartient à toute construction où la signature désigne son objet par une référence interne que le consommateur résout de son côté. La leçon générale est la même que celle de la clé de cache HTTP : quand deux composants d’une chaîne n’identifient pas le même objet à partir du même message, l’écart devient une vulnérabilité.

Trois caractéristiques se cumulent, et elles expliquent que cette classe d’attaques ait traversé une génération d’implémentations.

La flexibilité du format. XML autorise des commentaires, des espaces de noms multiples, des entités, des représentations équivalentes d’un même contenu, et des points d’extension à peu près partout. La spécification SAML en prévoit explicitement, dans son chapitre sur l’extensibilité : des extensions de schéma et des points d’extension par caractère générique. Un attaquant dispose donc d’emplacements légaux où ranger un fragment sans casser la validation de schéma. On retrouve ici la famille de problèmes que la page sur les entités externes XML traite par ailleurs : la puissance du format est sa faiblesse.

La séparation des rôles entre bibliothèques. La vérification de signature est faite par une bibliothèque cryptographique, le traitement du contenu par le code applicatif, et il n’existe pas d’interface naturelle entre les deux pour transmettre « voici l’élément exact qui était couvert ». La plupart des interfaces répondent par un booléen. Toute l’ingénierie correcte consiste à refuser ce booléen et à exiger de la bibliothèque qu’elle rende l’arbre vérifié.

La canonisation. Avant d’être condensées, les données référencées sont normalisées par un algorithme de canonisation, dont le rôle est de rendre équivalents des documents sémantiquement identiques mais écrits différemment. Cette normalisation est nécessaire, et elle est aussi une surface d’attaque : tout ce qu’elle retire ou réordonne peut être modifié sans changer le condensé. Il faut donc savoir, précisément, quelle transformation a été appliquée, ce que la contrainte SAML d’une unique référence et d’une signature enveloppée vise à rendre prévisible.

Six règles, dans l’ordre d’importance.

N’exploiter que ce qui a été vérifié. C’est la seule parade complète. Le code applicatif ne doit jamais relire le document source ; il doit recevoir, de l’étape de vérification, la référence exacte de l’élément couvert, et travailler sur celui-là. Formulé autrement : la vérification ne renvoie pas un verdict, elle renvoie un sous-arbre. Toute architecture où le vérificateur dit « oui » et où le lecteur repart du document complet est vulnérable par construction, quelle que soit la qualité de la bibliothèque.

Refuser les identifiants dupliqués. La spécification l’exige, et un document où un identifiant apparaît deux fois doit être rejeté sans tentative de résolution. C’est un contrôle de trois lignes qui ferme la variante la plus discrète.

Imposer les contraintes de SAML plutôt que de tolérer la généralité de XML Signature. Une référence et une seule, de type même-document, vers l’identifiant de l’élément racine de l’assertion ou du message. Une signature enveloppée. Toute autre forme, référence externe, référence multiple, signature détachée, est hors profil et doit être refusée, même si la bibliothèque sait la traiter. Accepter plus que ce que la spécification autorise est la définition opérationnelle d’une surface d’attaque gratuite.

Vérifier que la signature couvre ce dont on se sert, et pas un ancêtre vague. En cas d’héritage de signature, contrôler explicitement que la signature de l’élément englobant couvre l’assertion lue et tous ses enfants, comme la spécification le demande. L’idée générale, qui vaut au-delà de SAML, est qu’une signature sur un conteneur n’autorise pas à faire confiance à un contenu quelconque de ce conteneur.

Contrôler le reste de l’assertion, pas seulement sa signature. Une assertion authentique peut être rejouée ou présentée à un destinataire qui n’était pas prévu, défaut traité dans l’assertion fédérée injectée. La signature est une condition nécessaire ; la vérification du destinataire, de la fenêtre temporelle, de l’émetteur attendu et de l’unicité de l’identifiant de l’assertion l’est tout autant.

Préférer un format qui n’a pas ce problème, quand c’est possible. La raison pour laquelle cette classe d’attaques n’existe pas avec les jetons JWS est structurelle : le jeton est signé par valeur, dans une chaîne de caractères dont la signature couvre l’intégralité, et il n’y a aucune référence interne à résoudre. Le détail de cette construction est dans JWS, et la comparaison d’ensemble entre les deux mondes de la fédération dans OIDC, SAML et OAuth 2.1 en fédération. Cela ne signifie pas que SAML doive être remplacé partout, mais qu’une nouvelle intégration n’a plus de raison de le choisir, et qu’une intégration existante mérite une revue de ce point précis.

Sans exploitation du seul sous-arbre effectivement vérifié, une bibliothèque de signature parfaitement correcte valide un document dont l’application lit une autre partie, et l’attaquant choisit librement l’identité et les attributs qu’il obtient. Sans refus des identifiants dupliqués, le vérificateur et l’analyseur applicatif peuvent résoudre le même identifiant vers deux éléments différents, et la faille ne laisse aucune trace d’invalidité. Sans restriction au profil de signature imposé par la spécification, une référence externe ou multiple ouvre des chemins que la bibliothèque sait traiter et que personne n’a prévus. Sans contrôle que la signature d’un élément englobant couvre réellement l’assertion lue, l’héritage de signature autorise l’insertion de n’importe quel contenu dans un conteneur signé. Et sans les contrôles de destinataire, de fenêtre temporelle et d’unicité qui accompagnent la signature, une assertion authentique obtenue ailleurs reste présentable telle quelle.

Dans la connexion fédérée à l’ancienne, un fournisseur d’identité envoie à une application un document XML qui dit « cette personne est bien untel », et il le signe électroniquement pour que l’application puisse vérifier qu’il n’a pas été modifié.

La signature XML a une particularité qu’il faut comprendre : elle ne signe pas le document entier. Elle contient une référence qui désigne le morceau signé par un numéro, et c’est ce morceau-là qui est protégé. Tout le reste du document peut être modifié librement.

L’attaque consiste donc à déplacer le morceau signé ailleurs dans le document, à un endroit que l’application ne regarde pas, puis à mettre à sa place un morceau fabriqué de toutes pièces et non signé, qui dit par exemple « cette personne est l’administrateur ». La signature n’a pas été touchée, et le morceau qu’elle désigne est toujours là, intact. La vérification répond donc « document valide ». Mais l’application, elle, lit le morceau qui se trouve à la place habituelle, c’est-à-dire le faux.

Il n’y a aucune faiblesse mathématique là-dedans. La faille vient de ce que deux programmes désignent le morceau important de deux manières différentes : l’un par son numéro, l’autre par sa place. La seule défense qui marche consiste à ce que le programme de vérification ne réponde pas « oui » mais « voici précisément le morceau que j’ai vérifié », et que l’application n’utilise que celui-là.

Trois précautions complètent cela : refuser un document où un même numéro apparaît deux fois, n’accepter que la forme de signature prévue par la spécification et aucune autre, et ne pas conclure qu’un morceau est authentique simplement parce qu’il se trouve dans un document signé. Cette famille d’attaques n’existe pas avec les jetons signés plus récents, parce que leur signature couvre tout leur contenu et qu’il n’y a aucune référence à résoudre.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.