User.Read.All : la reconnaissance silencieuse qui précède les vraies attaques
Étude de cas : empêcher une application compromise de cartographier votre annuaire pour un spear-phishing ciblé.
Le scénario
Une société de 2 000 employés utilise un tableau de bord RH externe qui consomme User.Read.All pour produire des organigrammes en temps réel. L'application est en place depuis quatre ans, personne ne la remet en question, elle fonctionne.
Un stagiaire chez le fournisseur RH pousse par erreur une clé API dans un dépôt GitHub public. L'attaquant, qui scanne les leaks en continu, ramasse la clé le mardi. Pendant 72 heures, silence apparent : personne côté client ne remarque quoi que ce soit d'inhabituel. L'application RH a toujours fait beaucoup d'appels /users.
En réalité, l'attaquant télécharge l'annuaire complet. Noms, titres, managers, départements, téléphones, adresses de bureau, dates d'embauche. Six semaines plus tard, un courriel parfaitement calibré part de jean.tremblay-dg@entreprise-lookalike.com vers la contrôleuse financière de la filiale britannique. Objet : une demande de virement urgent signée du nom de son VP Finance, avec la bonne hiérarchie, le bon ton, le bon contexte projet. 840 000 £ partent.
L'enquête post-incident retrouve l'origine trois mois plus tard : la clé du fournisseur RH. L'audit Microsoft n'a jamais levé de drapeau, et c'était normal. Aucune règle n'a été violée.
Pourquoi Microsoft ne bloque pas ce scénario
User.Read.All est par conception la permission qui sert les annuaires, les organigrammes, les outils RH, les analyses Power BI, les CRM qui enrichissent des contacts. C'est la permission la plus accordée du catalogue Graph, après User.Read.
Le problème n'est pas la permission en soi. C'est l'absence totale de granularité sur son usage :
- Aucune distinction entre lire les 50 managers d'un département et télécharger les 2 000 utilisateurs du tenant.
- Aucune distinction entre nom et adresse courriel et titre, manager, département, téléphone. Tous ces attributs sortent par le même appel
/users. - L'audit Graph enregistre « l'app a lu /users », pas « l'app a énuméré 2 000 utilisateurs avec projection complète ».
- Defender for Cloud Apps peut flaguer les motifs de scraping massif, mais avec retard, et pour un tenant actif la signature d'un RH normal est difficile à distinguer d'une énumération.
C'est la permission la plus dangereuse parce que son usage malveillant est indiscernable de son usage légitime avec l'instrumentation native. Même logique que pour les autres permissions Graph à haut privilège : un interrupteur côté Microsoft, pas de pare-feu applicatif. Voir aussi l'angle mort des permissions Graph.
Ce que l'incident aurait donné avec un périmètre en place
Avec les appels /users routés à travers un proxy de gouvernance. Modèle : default-deny.
Règle 1 — Attributs autorisés (projection contrôlée)
- name: "HR dashboard — field projection"
endpoint_pattern: "/users"
http_methods: [GET]
conditions:
all_of:
- caller_identity:
application_id: "app-hr-dashboard"
- query_select:
allowed: ["displayName", "mail", "jobTitle", "department", "manager"]
deny_all_others: true
action: allow
L'application RH a besoin de cinq attributs pour faire son organigramme. Le proxy force $select sur la requête sortante et rejette les requêtes qui demandent mobilePhone, officeLocation, employeeHireDate ou l'étoile. L'attaquant perd les données de contexte qui rendent le spear-phishing convaincant.
Règle 2 — Pagination bornée
- name: "HR dashboard — pagination cap"
endpoint_pattern: "/users"
conditions:
all_of:
- pagination_cap:
max_pages: 5
page_size_max: 100
scope: "caller_identity"
window_seconds: 3600
action: allow
500 utilisateurs par heure suffisent pour un rafraîchissement RH normal. Au-delà, c'est de l'énumération. Le cap préventif coupe le téléchargement intégral de l'annuaire avant qu'il ne se produise.
Règle 3 — Périmètre de l'annuaire
- name: "HR dashboard — scope to business units"
endpoint_pattern: "/users"
conditions:
all_of:
- query_filter_required:
path: "companyName"
in: ["Acme Canada", "Acme France"]
action: allow
L'application RH n'a de contrat que pour deux filiales. La règle force un filtre $filter sur companyName dans chaque requête. Les utilisateurs de la filiale britannique restent structurellement invisibles à l'application, et donc à l'attaquant.
Règle 4 — Détection de motif d'énumération
- name: "HR dashboard — enumeration pattern"
endpoint_pattern: "/users"
conditions:
any_of:
- pattern_detect:
signal: "sequential_skiptoken"
threshold: 10
- pattern_detect:
signal: "wildcard_filter"
action: block
Dix requêtes $skiptoken consécutives, ou un filtre startswith(displayName, '') qui ratisse tout, sont deux signatures classiques de reconnaissance. Le blocage préventif coupe le scraping avant la cinquième page.
Règle 5 — Protection structurelle des cadres
- name: "HR dashboard — executive shield"
endpoint_pattern: "/users/*"
http_methods: [GET]
conditions:
any_of:
- object_attribute:
path: "memberOf.displayName"
contains: "Executive-Shield"
action: block
Le groupe Executive-Shield regroupe les utilisateurs à haut risque de ciblage (C-suite, directions finance, RH). Les applications RH standard ne lisent pas ces profils. Les outils qui en ont besoin sont tagués explicitement, les autres sont bloqués par identité. Le profil de la contrôleuse financière devient inaccessible à l'app RH compromise, même en lecture.
Les conditions query_select, pagination_cap et pattern_detect encadrent les patterns de reconnaissance. Voir la documentation pour la grammaire complète.
Ce que ça change pour le décideur
La reconnaissance précède l'attaque ciblée de plusieurs semaines. Les SIEM classiques ne la détectent pas parce qu'elle se fait à travers une application légitime avec une permission légitime. L'incident de 840 000 £ du scénario est attribuable à la permission lue plusieurs mois plus tôt, mais personne ne fait le lien en temps réel.
Avec une couche de gouvernance, trois bascules.
- La projection des attributs devient explicite. Chaque application voit ce dont elle a besoin, pas ce que Graph expose par défaut.
- La reconnaissance de masse devient détectable et bloquable. Le coût d'une fuite de clé est immédiatement visible au proxy, pas six mois plus tard dans une enquête forensique.
- Les cibles à forte valeur sont protégées par groupe, pas par obscurité. Le bouclier cadres est une règle, pas une convention.
Les limites qu'il faut nommer
La règle de projection (query_select) demande un inventaire précis de ce dont chaque application a réellement besoin. Beaucoup de déploiements RH ne documentent jamais cette liste. La démarche de mise en place d'un périmètre force ce travail, et c'est en pratique une des valeurs opérationnelles du projet.
Le groupe Executive-Shield demande une maintenance. Quand quelqu'un est promu ou part, le groupe doit être à jour. C'est un contrôle vivant, pas une règle set and forget.
Dernière limite : le BEC ne vient pas toujours de l'annuaire Graph. LinkedIn, les signatures de courriels, les rapports annuels publics sont aussi des sources. La règle ne couvre qu'un vecteur. Elle le couvre bien.
Ce qu'il faut retenir
User.Read.All est la permission la plus discrète de la famille des permissions à haut risque. Elle ne déclenche ni alerte, ni DLP, ni enquête. Elle prépare les attaques qui, elles, coûtent cher.
Pour une organisation qui se soucie de ses équipes finance, exécutives ou légales, encadrer User.Read.All par une couche de gouvernance déclarative revient moins cher que d'expliquer, après coup, comment 840 000 £ sont partis vers un compte que personne ne reconnaît.
C'est exactement pour ça que GraphWarden existe.