In this section
OAuth Triage: Scope and Blast Radius of a Malicious App
The same permission, two incidents
Two consent grants land on your desk in the same hour, both flagged by the same rule. Both show the scope Mail.Read. The first turns out to be one mailbox: a single user authorized an app to read their own mail, and the exposure stops there. The second is every mailbox in the organization: a tenant-wide grant that lets an application read the CEO's mail, the legal team's mail, the entire company's mail. You size the first as a contained incident and the second as the breach that defines the company's year. The scope string on both was identical.
If you size those two the same way, you have either over-escalated a one-user problem into a company-wide panic or, far worse, under-escalated a company-wide breach into a one-user ticket. The field that separates them is not in the scope name at all. This sub is about the factor that turns a scope into an actual data-reach number, and why reading it correctly is the difference between an incident you describe in one sentence and one that triggers organization-wide breach notification.
This is the next move after reading the consent grant. Decomposing a consent told you what the application is and what it asked for; sizing the blast radius tells you how much of the organization is on the line if that grant is malicious. Blast radius is the full set of data a compromised application can reach with the permissions it holds, and it is determined by two things working together: the scopes that were granted, and the permission type those scopes belong to. You already know how to read the scopes (the Resource.Action.Audience pattern). What this sub adds is the second factor, the one analysts consistently misread: the distinction between delegated and application permissions.
Delegated vs application permissions
OAuth has two fundamentally different permission models, and the same scope name behaves completely differently depending on which one a consent uses. This is the single most important concept in sizing an application's blast radius.
A delegated permission lets the application act on behalf of the user who signed in. The app's access is the intersection of what the app was granted and what the user can do. A delegated Mail.Read lets the application read the mail of the user who consented, and only that user. If a standard employee consents, the app reaches that employee's mailbox. The user is always present in the access equation: the app borrows the user's permissions and can never exceed them.
An application permission lets the application act as itself, with no user involved. The app authenticates with its own credential and accesses data directly through the API, bounded only by the permission it was granted. An application Mail.Read (which in practice appears as Mail.Read granted as an application permission, often with admin consent) lets the application read every mailbox in the tenant, because there is no user to bound it. The app is not borrowing anyone's access; it has its own, and its own is tenant-wide.
A note on the suffix, because it can look like it contradicts what you learned about reading scopes. The .All suffix marks the tenant-wide variant within the delegated catalog, but the application-permission catalog is a separate list, and many application permissions are tenant-wide without carrying an .All suffix at all. That is exactly why the permission type is the rule and the suffix is only a hint. You cannot size breadth from the scope string alone; you read whether the permission was granted as delegated or as application, and that field is what tells you whether "every mailbox" is in play.
The same scope name, two permission models, two completely different blast radii. The permission type is the field that decides whether the app reaches one mailbox or all of them.
The trap is that the scope name alone does not always tell you which model you are looking at. You have to read the permission type from the consent record or the application's granted permissions, not infer it from the scope string. Some scopes exist only as application permissions, some only as delegated, and some as both with the same name. The audit record distinguishes them, and reading that distinction is the difference between a correct and an incorrect blast-radius estimate.
The escalation that hides in plain sight
The most dangerous pattern in OAuth attacks is the application permission granted via admin consent. A delegated permission needs a user, and a user can only consent to access their own data. An application permission with admin consent has no such limit. When an attacker compromises an account with administrative rights (or escalates to one), the prize is not the admin's mailbox. The prize is the ability to grant an application a tenant-wide application permission, which converts a single compromised admin into standing access to every user's data.
This is why the consent type and the permission type compound. A user-consented delegated permission is bounded twice (one user, that user's access). An admin-consented application permission is bounded by nothing except the scope. The first is a contained incident; the second is the kind of breach that defines a company's year. When you triage a consent, reading whether it is an application permission with admin consent is the highest-priority field, because it is the one that can turn a routine alert into a tenant-wide exposure.
The two fields are not independent. Admin consent on an application permission removes every bound, which is why that one cell is the highest-priority read in OAuth triage.
A practical note on how attackers reach the bottom-right cell. They rarely start with admin rights. The common path is: compromise a standard user through consent phishing or token theft, use that foothold to escalate (a directory-role assignment, a privileged-role-eligible account, a misconfigured group), and once they hold admin, grant an application a tenant-wide application permission. The application permission is the objective, not the admin account itself. The admin compromise is the means; the standing tenant-wide data access is the end. This is why an admin-consent event for an application permission, appearing shortly after any privilege-escalation signal, is one of the highest-severity correlations in identity-and-application triage.
The common mistake
Sizing the blast radius from the scope name and stopping there. The analyst sees `Mail.Read`, assumes it means one mailbox (the delegated reading), and sizes the incident as a single-user exposure. But the consent was an application permission with admin consent, and the app can read every mailbox in the organization. The scope name was identical to the benign case; the permission type was the entire difference. Always read the permission type (delegated vs application) and the consent type (user vs admin) before you state a blast radius. The scope tells you what action the app can take; the permission type tells you whose data it can take it against.
Mapping scopes to data reach
Once you know the permission type, you can map the granted scopes to the actual data the application can reach. The mapping is the blast-radius estimate, and it is what you hand to the incident lead and the data-protection function to drive notification decisions.
The estimate has two dimensions. The first is breadth: how many principals' data is reachable (one user for delegated, the whole tenant for application permissions). The second is depth: what categories of data the scopes unlock (mail, files, calendar, contacts, directory, Teams messages). A delegated Mail.Read plus Files.Read reaches one user's mail and files. An application Mail.Read.All plus Files.Read.All reaches every user's mail and files. The breadth comes from the permission type; the depth comes from the set of scopes.
Depth deserves its own scrutiny because attackers frequently request a bundle of scopes that, read individually, each look modest, but read together describe a complete data-theft toolkit. A grant of Mail.Read, Contacts.Read, and Files.Read on a single user is not three small exposures; it is one complete exposure of that person's communications, network, and documents. When you list the depth, list the union and read it as a whole. The attacker chose that combination for a reason, and the reason is usually that the combination reaches everything they want from the target. A useful discipline is to translate each scope into the plain-language question it answers for the attacker: Mail.Read answers "what is this person discussing," Contacts.Read answers "who do they know," Files.Read answers "what are they working on." Stated that way, the depth of even a single-user grant becomes obvious to the incident lead who has to decide on notification.
The blast radius is the intersection of how many principals the app can reach and how many data categories its scopes unlock. The bottom-left is a contained incident; the top-right is an organization-wide breach.
Seeing it in the evidence
A generic worked example. Two applications have been flagged in a tenant, and you need to size each one's blast radius before deciding which to contain first. Both show a Mail.Read scope in their granted permissions. The naive reading treats them as equivalent. The permission type tells the real story.
Where to find it
The application's granted permissions, in the enterprise-applications or app-registrations view of the identity provider, or in the consent audit record. The permission type (delegated vs application) is a distinct field on each granted scope. The consent type (user vs admin) is recorded alongside.
How to extract it
Enumerate the application's OAuth2 permission grants (delegated) and app-role assignments (application permissions) separately. The two live in different places: delegated grants are per-user or per-tenant consent records; application permissions are app-role assignments on the service principal. An application that has app-role assignments to a resource is operating with application permissions.
# Application (app-role) permissions on the service principal: tenant-wide reach
Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId $spId |
Select-Object PrincipalDisplayName, ResourceDisplayName, AppRoleId
# Delegated (OAuth2) permissions: bounded to consenting users
Get-MgServicePrincipalOauth2PermissionGrant -ServicePrincipalId $spId |
Select-Object ConsentType, PrincipalId, Scope
# Application permissions (app-role assignments) for the service principal
az ad sp show --id "$SP_ID" --query "appRoles" -o table
# Delegated permission grants
az rest --method get \
--url "https://graph.microsoft.com/v1.0/servicePrincipals/$SP_ID/oauth2PermissionGrants"
AuditLogs
| where OperationName has_any ("Add app role assignment", "Add delegated permission grant")
| extend App = tostring(TargetResources[0].displayName)
| project TimeGenerated, OperationName, App, InitiatedBy
| sort by TimeGenerated desc
Read the output
Predict the two outcomes. Application A returns a delegated OAuth2 permission grant: Mail.Read, consent type "Principal" (a single user), one principal ID. Its blast radius is that one user's mailbox. Application B returns an app-role assignment to the mail resource: Mail.Read as an application permission, granted tenant-wide. Its blast radius is every mailbox. Same scope string, two queries, two completely different answers. Application B is the immediate containment priority.
The output resolves the two applications cleanly. Application A's access came back as a delegated grant with a single principal and "Principal" consent type, which means one user consented for their own mailbox: a one-mailbox blast radius, a contained incident. Application B's access came back as an app-role assignment, which is an application permission operating tenant-wide: every mailbox in reach, an organization-scale exposure. The scope string Mail.Read was identical in both. The query that separated them was the one that read the permission type, and it changed the severity of Application B by the entire size of the organization.
This is the blast-radius estimate in practice. You do not size an incident from the scope name. You size it from the permission type (which sets breadth) and the full scope set (which sets depth), and you let that estimate drive the containment order and the notification scope.
Your turn
You find an application with three granted permissions: User.Read (delegated, one user), Mail.ReadWrite (delegated, one user), and Files.Read.All (application permission, admin-consented). The application name suggests a document-collaboration tool. How do you size the blast radius, and what is the most exposed data?
Reveal
The blast radius is split across two breadths. The two delegated permissions (User.Read, Mail.ReadWrite) reach only the consenting user: that user's profile and full read-write mail access. The application permission (Files.Read.All, admin-consented) reaches every file in the tenant, because application permissions are not bounded by a user. The most exposed data is organization-wide file content via Files.Read.All: that single permission converts the incident from a one-user mail exposure into a tenant-wide file exposure. Size it as a tenant-wide file breach plus a single-user mail compromise. Contain the application permission first (it has the larger blast radius), then the delegated grants. The document-collaboration name is irrelevant to the sizing; the permission types and scopes are the whole story.
Blast radius estimate: sizing an application's reach
For each flagged application, work these steps to produce a defensible blast-radius estimate that drives containment order and notification scope.
1. Separate delegated from application permissions
Enumerate OAuth2 permission grants (delegated) and app-role assignments (application) separately. They live in different places and have different breadth. An app-role assignment is the high-breadth case.
2. Determine breadth (who)
Delegated = the consenting user(s) only. Application = the whole tenant. Admin-consented application permission is the maximum breadth and the highest priority.
3. Determine depth (what data)
Map every scope to its data category: mail, files, contacts, calendar, directory, chat. The union of categories across all scopes is the depth of the exposure.
4. State the estimate as breadth x depth
"One user's mail" or "every mailbox plus every file in the tenant." This is the number the incident lead and data-protection function need to scope notification.
5. Never size from the scope name alone
Identical scope strings (Mail.Read) can be one mailbox or every mailbox depending on the permission type. Read the type every time. The scope is the action; the type is whose data it acts against.
Where this leads: you can separate delegated from application permissions and produce a defensible blast-radius estimate. Next, TR3.6 moves from what the application could reach to what it actually did, tracing the application's real data-access activity to confirm the exposure and distinguish a dormant grant from active data harvesting.