use crate::http_client::HttpClient;
use crate::types::{Confidence, ScanConfig, Severity, Vulnerability};
use anyhow::Result;
use std::sync::Arc;
use tracing::info;
pub struct AzureApimScanner {
http_client: Arc<HttpClient>,
}
impl AzureApimScanner {
pub fn new(http_client: Arc<HttpClient>) -> Self {
Self { http_client }
}
pub async fn scan(
&self,
url: &str,
_config: &ScanConfig,
) -> Result<(Vec<Vulnerability>, usize)> {
info!("[Azure-APIM] Scanning for Cross-Tenant Signup Bypass");
let mut vulnerabilities = Vec::new();
let mut tests_run = 0;
let base_url = match url::Url::parse(url) {
Ok(u) => u,
Err(_) => return Ok((vulnerabilities, 0)),
};
let host = match base_url.host_str() {
Some(h) => h,
None => return Ok((vulnerabilities, 0)),
};
tests_run += 1;
let is_apim = self.detect_apim_portal(url, host).await;
if !is_apim {
info!("[Azure-APIM] Not an Azure APIM Developer Portal, skipping");
return Ok((vulnerabilities, tests_run));
}
info!("[Azure-APIM] Detected Azure APIM Developer Portal");
let origin = format!("{}://{}", base_url.scheme(), host);
tests_run += 1;
let signup_accessible = self.check_signup_endpoint(&origin).await;
tests_run += 1;
let (basic_auth_active, api_response) = self.check_basic_auth_api(&origin).await;
tests_run += 1;
let signup_hidden = self.check_signup_hidden(&origin).await;
if basic_auth_active {
if signup_hidden {
info!("[ALERT] Azure APIM Cross-Tenant Signup Bypass detected!");
vulnerabilities.push(Vulnerability {
id: generate_uuid(),
vuln_type: "Azure APIM Cross-Tenant Signup Bypass".to_string(),
severity: Severity::High,
confidence: Confidence::High,
category: "Access Control".to_string(),
url: url.to_string(),
parameter: None,
payload: String::new(),
description: format!(
"CRITICAL: Azure APIM Developer Portal is vulnerable to cross-tenant signup bypass (GHSA-vcwf-73jp-r7mv). \
The Basic Authentication signup API is accessible even though signup is disabled in the UI. \
Attackers can register accounts by sending direct API requests, bypassing administrative controls. \
This enables cross-tenant account creation and potential access to API documentation, subscription keys, \
and other Developer Portal resources. CVSS: 6.5 (Medium-High)."
),
evidence: Some(format!(
"Signup API response: {}. Signup UI hidden: {}. Basic Auth API active: true",
api_response.as_deref().unwrap_or("active"),
signup_hidden
)),
cwe: "CWE-284".to_string(),
cvss: 6.5,
verified: true,
false_positive: false,
remediation: r#"IMMEDIATE ACTION REQUIRED:
1. REMOVE Basic Authentication identity provider completely in Azure Portal
- Navigate to APIM instance → Developer Portal → Identities
- DELETE the "Username and password" identity provider entirely
- NOTE: Simply disabling signup in UI is NOT sufficient!
2. Audit existing Developer Portal accounts
- Review all user accounts for unauthorized registrations
- Check account creation timestamps and patterns
- Remove any suspicious or unauthorized accounts
3. Enable Azure AD authentication only
- Configure Azure AD as the sole identity provider
- This enforces proper tenant boundaries
- Implement MFA for all portal users
4. Implement monitoring
- Enable Azure Monitor alerts for signup activity
- Log and review all Developer Portal authentication events
- Set up alerts for unusual registration patterns
Reference: https://github.com/bountyyfi/Azure-APIM-Cross-Tenant-Signup-Bypass"#.to_string(),
discovered_at: chrono::Utc::now().to_rfc3339(),
ml_confidence: None,
ml_data: None,
});
} else {
info!("[NOTE] Azure APIM with Basic Auth enabled (potential attack source)");
vulnerabilities.push(Vulnerability {
id: generate_uuid(),
vuln_type: "Azure APIM Basic Auth Enabled".to_string(),
severity: Severity::Medium,
confidence: Confidence::Medium,
category: "Access Control".to_string(),
url: url.to_string(),
parameter: None,
payload: String::new(),
description: format!(
"Azure APIM Developer Portal has Basic Authentication enabled with visible signup. \
While this is a configuration choice, it increases attack surface. \
This instance could potentially be used as an attack source to perform \
cross-tenant signup bypass attacks against other APIM instances. \
Consider migrating to Azure AD authentication for improved security."
),
evidence: Some(format!(
"Basic Auth signup API active: true. Signup visible in UI: {}",
signup_accessible
)),
cwe: "CWE-284".to_string(),
cvss: 4.0,
verified: true,
false_positive: false,
remediation: r#"RECOMMENDED ACTIONS:
1. Consider migrating to Azure AD authentication
2. Implement email domain whitelisting for registrations
3. Monitor signup activity for suspicious registrations
4. Review and remove unused developer accounts regularly
5. Enable MFA for all portal users
Reference: https://github.com/bountyyfi/Azure-APIM-Cross-Tenant-Signup-Bypass"#.to_string(),
discovered_at: chrono::Utc::now().to_rfc3339(),
ml_confidence: None,
ml_data: None,
});
}
}
info!(
"[SUCCESS] [Azure-APIM] Completed {} tests, found {} issues",
tests_run,
vulnerabilities.len()
);
Ok((vulnerabilities, tests_run))
}
async fn detect_apim_portal(&self, url: &str, host: &str) -> bool {
if host.contains("developer.azure-api.net") {
return true;
}
if let Ok(response) = self.http_client.get(url).await {
let body = &response.body;
let apim_indicators = [
"developerPortal",
"azure-api.net",
"apim-",
"api-management",
"ApiManagement",
"Developer Portal",
"API Management",
"Subscribe to API",
"API documentation",
];
let has_apim_header = response.headers.iter().any(|(k, v)| {
k.to_lowercase().contains("apim") || v.to_lowercase().contains("azure-api")
});
if has_apim_header {
return true;
}
for indicator in &apim_indicators {
if body.contains(indicator) {
return true;
}
}
}
false
}
async fn check_signup_endpoint(&self, origin: &str) -> bool {
let signup_url = format!("{}/signup", origin);
match self.http_client.get(&signup_url).await {
Ok(response) => response.status_code == 200 || response.status_code == 302,
Err(_) => false,
}
}
async fn check_basic_auth_api(&self, origin: &str) -> (bool, Option<String>) {
let signup_url = format!("{}/signup", origin);
let payload = serde_json::json!({
"challenge": {
"testCaptchaRequest": {
"challengeId": "00000000-0000-0000-0000-000000000000",
"inputSolution": "AAAAAA"
},
"azureRegion": "NorthCentralUS",
"challengeType": "visual"
},
"signupData": {
"email": "security-probe@nonexistent-invalid-domain.test",
"firstName": "Security",
"lastName": "Probe",
"password": "SecurityProbe123!",
"confirmation": "signup",
"appType": "developerPortal"
}
});
match self.http_client.post_json(&signup_url, &payload).await {
Ok(response) => {
let body_lower = response.body.to_lowercase();
if response.status_code == 404 {
return (false, Some("Signup API not found (404)".to_string()));
}
if response.status_code == 400 {
if body_lower.contains("captcha") || body_lower.contains("challenge") {
return (
true,
Some("Basic Auth signup API ACTIVE (captcha validation)".to_string()),
);
}
if body_lower.contains("email")
|| body_lower.contains("password")
|| body_lower.contains("invalid")
{
return (
true,
Some("Basic Auth signup API ACTIVE (input validation)".to_string()),
);
}
return (
true,
Some("Basic Auth signup API responds (400)".to_string()),
);
}
if response.status_code == 409 {
return (
true,
Some("Basic Auth signup API ACTIVE (409 conflict)".to_string()),
);
}
if response.status_code == 200 || response.status_code == 201 {
return (
true,
Some("Basic Auth signup API ACCEPTS requests".to_string()),
);
}
if response.status_code == 401 || response.status_code == 403 {
return (
true,
Some(format!(
"Basic Auth signup API responds ({})",
response.status_code
)),
);
}
if response.status_code == 422 {
return (
true,
Some("Basic Auth signup API validates (422)".to_string()),
);
}
(
false,
Some(format!("Signup returned {}", response.status_code)),
)
}
Err(_) => (false, None),
}
}
async fn check_signup_hidden(&self, origin: &str) -> bool {
let signup_url = format!("{}/signup", origin);
match self.http_client.get(&signup_url).await {
Ok(response) => {
if response.status_code == 404 {
return true;
}
if response.status_code >= 300 && response.status_code < 400 {
return true;
}
false
}
Err(_) => false,
}
}
}
fn generate_uuid() -> String {
use rand::Rng;
let mut rng = rand::rng();
format!(
"{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
rng.random::<u32>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u16>(),
rng.random::<u64>() & 0xffffffffffff
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_apim_domain_detection() {
assert!("example.developer.azure-api.net".contains("developer.azure-api.net"));
assert!("contoso.developer.azure-api.net".contains("developer.azure-api.net"));
}
#[test]
fn test_uuid_generation() {
let uuid = generate_uuid();
assert_eq!(uuid.len(), 36); assert!(uuid.contains('-'));
}
}