Application.ReadWrite.All : la permission qui accorde toutes les autres
Étude de cas : empêcher l'escalade de privilèges par une app de provisioning compromise.
Le scénario
Un MSP infogérance Microsoft 365 gère 120 tenants clients. Son outil de provisioning crée automatiquement les applications Entra ID pour chaque nouveau client qui s'onboarde : un jeu standardisé de quatre apps (monitoring, backup, reporting, ticketing). L'outil détient Application.ReadWrite.All dans chaque tenant, accordé à l'installation.
Un mardi soir, un ingénieur du MSP reçoit un courriel d'alerte : l'outil vient de créer une nouvelle application dans 37 tenants, appelée O365-Compliance-Sync, et lui a accordé Mail.ReadWrite.All, Files.ReadWrite.All, et User.Read.All. Tout de suite après, l'application a commencé à lire les boîtes et les SharePoints.
Un serveur CI du MSP avait été compromis la veille. L'attaquant a récupéré le secret applicatif de l'outil de provisioning. Puisque ce secret ouvrait les 120 tenants, et puisque la permission Application.ReadWrite.All permet de s'accorder n'importe quelle autre permission, le reste était mécanique.
L'alerte est arrivée parce que le MSP avait configuré un webhook personnalisé sur les nouvelles créations d'apps. Sans ce webhook, rien. Les audits natifs Entra ID voient l'événement, mais personne ne les lit en temps réel.
Pourquoi Microsoft ne bloque pas ce scénario
Application.ReadWrite.All est la permission racine de la gestion d'applications dans Entra ID. Elle est par conception suffisante pour créer une application, lui affecter un service principal, lui accorder une permission, et signer son consentement administrateur à la place d'un humain. C'est exactement ce que les outils de provisioning légitimes font.
Le problème est qu'il n'existe pas de sous-ensemble natif. Il n'y a pas de Application.ReadWrite.AllExceptHighRisk ni de Application.ReadWrite.SelfAndDescendants. Une fois la permission accordée, l'app peut accorder n'importe quelle autre permission Graph, y compris celles réservées normalement aux Global Admins.
- Le consentement administrateur requis pour une permission élevée peut être pré-accordé par l'app qui détient
Application.ReadWrite.All, via les endpoints/oauth2PermissionGrantset/appRoleAssignments. - Les Conditional Access Policies pour applications fonctionnent sur les utilisateurs, pas sur les appels Graph applicatifs.
- Les policies de gouvernance d'apps (Entra App Governance, Defender for Cloud Apps) détectent les apps à risque, souvent après leur première utilisation effective.
- L'audit Entra enregistre l'événement de création, mais les équipes sécurité l'examinent rarement en quasi-temps réel.
Le pattern est structurellement identique à ce qu'on décrivait pour Sites.FullControl.All, en plus grave : c'est la permission d'escalade, pas juste de pivot.
Ce que l'incident aurait donné avec un périmètre en place
Avec les appels Entra ID (endpoints /applications, /servicePrincipals, /oauth2PermissionGrants, /appRoleAssignments) routés à travers un proxy de gouvernance. Modèle : default-deny.
Règle 1 — Création d'apps limitée à un template nommé
- name: "Provisioning — app creation allowlist"
endpoint_pattern: "/applications"
http_methods: [POST]
conditions:
all_of:
- caller_identity:
application_id: "app-provisioning-msp"
- request_body:
path: "displayName"
matches_any: ["MSP-Monitor-*", "MSP-Backup-*", "MSP-Report-*", "MSP-Ticket-*"]
action: allow
L'outil de provisioning crée quatre types d'apps avec des noms standardisés. Toute création hors du pattern légitime est refusée. L'app O365-Compliance-Sync du scénario ne pourrait jamais être créée.
Règle 2 — Permissions accordables limitées à une allowlist
- name: "Provisioning — permission grant allowlist"
endpoint_pattern: "/servicePrincipals/*/appRoleAssignments"
http_methods: [POST]
conditions:
all_of:
- request_body:
path: "appRoleId"
in_reference: "approved_graph_roles"
action: allow
La liste approved_graph_roles contient les appRoleId GUID des permissions dont les apps MSP ont effectivement besoin (lecture seule, scope restreint). Toute tentative d'accorder Mail.ReadWrite.All, Files.ReadWrite.All, Directory.ReadWrite.All ou Application.ReadWrite.All est refusée, même si le demandeur est l'outil de provisioning légitime.
Règle 3 — Blocage structurel des permissions à très haut risque
- name: "Global — never grant tier-zero"
endpoint_pattern: "/servicePrincipals/*/appRoleAssignments"
http_methods: [POST]
conditions:
any_of:
- request_body:
path: "appRoleId"
in_reference: "tier_zero_graph_roles"
action: block
La liste tier_zero_graph_roles contient les permissions que aucune application du tenant ne devrait s'accorder à elle-même, peu importe l'outil : RoleManagement.*.Directory, Application.ReadWrite.All, Directory.ReadWrite.All. C'est une ceinture de sécurité globale, insensible à l'identité de l'appelant.
Règle 4 — Approval flow pour les opérations destructrices
- name: "Provisioning — app delete requires approval"
endpoint_pattern: "/applications/*"
http_methods: [DELETE]
conditions:
all_of:
- external_approval:
webhook: "https://msp-approvals.internal/graph-delete"
timeout_seconds: 300
action: allow
Les suppressions d'applications attendent une validation humaine. Le proxy appelle un webhook interne, qui déclenche une notification dans le système d'approbation du MSP. Cinq minutes pour répondre, sinon refus. L'attaquant qui voudrait effacer ses traces est ralenti par un contrôle humain.
Règle 5 — Débit et fenêtre
- name: "Provisioning — rate + window"
endpoint_pattern: "/applications"
http_methods: [POST, PATCH, DELETE]
conditions:
all_of:
- rate_limit:
max: 5
window_seconds: 3600
scope: "caller_identity"
- time_window:
timezone: "America/Montreal"
days: ["Mon", "Tue", "Wed", "Thu", "Fri"]
hours: "08:00-18:00"
action: allow
Cinq opérations d'app par heure suffisent pour un onboarding normal, même pour un MSP avec plusieurs tenants. La rafale de 37 créations du scénario serait coupée à la cinquième. La fenêtre temporelle recouvre le vecteur classique attaque de nuit pour passer sous le radar.
Les allowlists et denylists par appRoleId, le webhook d'approbation et la détection de rafales sont des conditions de base de GraphWarden. Voir la documentation pour la grammaire complète.
Voir la documentation des règles →Ce que ça change pour le décideur
L'escalade par Application.ReadWrite.All est le scénario qui transforme une compromission d'un poste de développeur en compromission totale du tenant. Les outils natifs détectent, les détections préviennent peu. La différence se mesure en nombre de tenants touchés avant la première intervention humaine.
Avec une couche de gouvernance devant Graph, trois bascules.
- Les permissions à haut risque ne peuvent plus être accordées automatiquement. La règle tier-zero coupe l'escalade à la racine, indépendamment de l'outil qui la demande.
- Les opérations destructrices demandent une validation humaine. Un attaquant pressé perd l'avantage du temps.
- L'audit contient déjà le signal de l'incident. Chaque tentative de grant refusée est un indicateur d'alerte à forte valeur, directement corrélable à une identité applicative.
Les limites qu'il faut nommer
Les allowlists de permissions (approved_graph_roles, tier_zero_graph_roles) doivent être maintenues. La liste tier-zero évolue peu, c'est un jeu stable d'une vingtaine de permissions. L'allowlist MSP évolue avec les besoins applicatifs, ce qui demande un processus léger mais régulier.
Le webhook d'approbation introduit une dépendance externe. Si le système d'approbation est en panne ou en maintenance, l'opération est refusée (fail-safe). C'est le bon arbitrage pour des opérations destructrices, pas pour du trafic transactionnel.
Dernière limite : le proxy doit être sur le chemin. Pour un outil de provisioning multi-tenant, cela signifie que chaque tenant client doit router son trafic Graph par un endpoint dédié. L'architecture est rodée, c'est d'ailleurs le cas d'usage principal des déploiements MSP de GraphWarden.
Ce qu'il faut retenir
Application.ReadWrite.All est la permission la plus mal dosée du catalogue Graph : elle est nécessaire à tout outil de provisioning sérieux, et simultanément la racine de la quasi-totalité des escalades tenant-wide. Sa version safe n'existe pas côté Microsoft.
La question n'est pas de savoir si un outil avec cette permission va finir par être compromis. La question est de savoir si votre architecture lui permet, le jour où ça arrive, d'accorder à d'autres applications toutes les permissions Graph restantes. Avec une couche de gouvernance, la réponse est non, par construction.
C'est exactement pour ça que GraphWarden existe.