Permissive Cross-domain Policy with Untrusted Domains
Description
Permissive Cross-domain Policy with Untrusted Domains occurs when software uses a cross-domain policy file (such as crossdomain.xml or clientaccesspolicy.xml) that includes domains that should not be trusted. These policy files control which external domains can make cross-origin requests to a web application. Overly permissive policies, especially those using wildcards (*), allow any website to interact with the application, potentially leading to data theft, CSRF attacks, and other cross-origin vulnerabilities.
Risk
Overly permissive CORS policies and cross-domain files expose applications to significant risk. Attackers can make authenticated requests from malicious sites, stealing user data or performing unauthorized actions. The wildcard (*) in Access-Control-Allow-Origin combined with credentials allows any site to impersonate users. Misconfigured CORS has led to numerous data breaches where attackers extracted sensitive information from authenticated sessions. APIs commonly misconfigure CORS, exposing internal data.
Solution
Never use wildcard (*) for Access-Control-Allow-Origin when credentials are involved. Maintain a strict whitelist of allowed origins. Validate the Origin header against the whitelist before reflecting it. Don't trust user-supplied origins. Remove or restrict crossdomain.xml and clientaccesspolicy.xml files. Implement proper CORS headers with specific origins. Use SameSite cookies as additional protection. Regularly audit cross-origin configurations.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Data Theft Malicious sites can read responses from the vulnerable application, stealing user data. |
| Integrity | Scope: Unauthorized Actions Cross-origin requests can perform state-changing operations as the authenticated user. |
| Access Control | Scope: Authentication Bypass Attackers can access authenticated resources by leveraging user sessions. |
Example Code + Solution Code
Vulnerable Code
<!-- VULNERABLE: crossdomain.xml with wildcard -->
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<allow-access-from domain="*" /> <!-- Allows ANY domain! -->
</cross-domain-policy>
<!-- VULNERABLE: clientaccesspolicy.xml -->
<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*"/> <!-- Allows ANY domain! -->
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
# VULNERABLE: Wildcard CORS with credentials
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app, origins="*", supports_credentials=True) # DANGEROUS!
# VULNERABLE: Reflecting origin without validation
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
# Reflects ANY origin!
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
# VULNERABLE: Regex bypass
ALLOWED_ORIGINS_PATTERN = r'.*\.example\.com'
@app.after_request
def add_cors_regex(response):
origin = request.headers.get('Origin')
if re.match(ALLOWED_ORIGINS_PATTERN, origin):
# Attacker uses: evil.com.example.com or evil-example.com
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
// VULNERABLE: Spring CORS with wildcard
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // Any origin!
.allowCredentials(true) // With credentials!
.allowedMethods("*");
}
}
// VULNERABLE: Filter reflecting origin
@WebFilter("/*")
public class VulnerableCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("Origin");
// Reflects any origin!
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(req, res);
}
}
// VULNERABLE: Express with permissive CORS
const cors = require('cors');
app.use(cors({
origin: true, // Reflects any origin
credentials: true
}));
// VULNERABLE: Manual CORS with reflection
app.use((req, res, next) => {
// Reflects whatever origin is sent
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
// VULNERABLE: Insufficient origin check
app.use((req, res, next) => {
const origin = req.headers.origin;
// Weak check - attacker uses example.com.evil.com
if (origin && origin.includes('example.com')) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
next();
});
Fixed Code
<!-- SAFE: Specific domains only -->
<?xml version="1.0"?>
<!DOCTYPE cross-domain-policy SYSTEM "http://www.adobe.com/xml/dtds/cross-domain-policy.dtd">
<cross-domain-policy>
<site-control permitted-cross-domain-policies="master-only"/>
<allow-access-from domain="app.example.com" secure="true"/>
<allow-access-from domain="www.example.com" secure="true"/>
</cross-domain-policy>
<!-- Or better: Remove if not needed -->
<!-- If Flash/Silverlight not used, delete crossdomain.xml entirely -->
# SAFE: Strict origin whitelist
from flask import Flask, request
from flask_cors import CORS
app = Flask(__name__)
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://www.example.com',
'https://admin.example.com'
}
def check_origin(origin):
return origin in ALLOWED_ORIGINS
CORS(app, origins=check_origin, supports_credentials=True)
# SAFE: Manual CORS with strict validation
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
if origin and origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Max-Age'] = '86400'
# Never use wildcard with credentials
# Don't reflect untrusted origins
return response
# SAFE: Proper subdomain validation
def is_valid_origin(origin):
if not origin:
return False
try:
from urllib.parse import urlparse
parsed = urlparse(origin)
# Must be HTTPS
if parsed.scheme != 'https':
return False
# Exact match or valid subdomain
allowed_domains = ['example.com', 'example.org']
host = parsed.netloc.lower()
for domain in allowed_domains:
if host == domain or host.endswith('.' + domain):
return True
return False
except:
return False
// SAFE: Spring CORS with whitelist
@Configuration
public class SecureCorsConfig implements WebMvcConfigurer {
private static final List<String> ALLOWED_ORIGINS = List.of(
"https://app.example.com",
"https://www.example.com",
"https://admin.example.com"
);
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins(ALLOWED_ORIGINS.toArray(new String[0]))
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("Content-Type", "Authorization")
.maxAge(86400);
}
}
// SAFE: Custom CORS filter with validation
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class SecureCorsFilter implements Filter {
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://app.example.com",
"https://www.example.com"
);
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String origin = request.getHeader("Origin");
if (origin != null && ALLOWED_ORIGINS.contains(origin)) {
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
response.setHeader("Access-Control-Max-Age", "86400");
}
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}
// SAFE: Using Spring Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(corsConfigurationSource());
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(
"https://app.example.com",
"https://www.example.com"
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(List.of("*"));
configuration.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
// SAFE: Express with strict origin whitelist
const cors = require('cors');
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://www.example.com',
'https://admin.example.com'
]);
const corsOptions = {
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, etc.)
// Only if you specifically need this
if (!origin) {
return callback(null, true);
}
if (ALLOWED_ORIGINS.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400
};
app.use(cors(corsOptions));
// SAFE: Manual implementation with strict validation
function validateOrigin(origin) {
if (!origin) return false;
try {
const url = new URL(origin);
// Must be HTTPS
if (url.protocol !== 'https:') return false;
// Check against whitelist
const allowedHosts = ['app.example.com', 'www.example.com'];
return allowedHosts.includes(url.hostname);
} catch {
return false;
}
}
app.use((req, res, next) => {
const origin = req.headers.origin;
if (validateOrigin(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Max-Age', '86400');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
Exploited in the Wild
Facebook CORS Misconfiguration (2019)
A CORS misconfiguration in Facebook's API allowed attackers to read private data by making cross-origin requests from malicious websites.
Various API Data Leaks
Multiple organizations have suffered data breaches due to CORS misconfigurations in their APIs, allowing any website to extract user data from authenticated sessions.
Adobe Flash Cross-Domain Attacks
Before Flash's deprecation, permissive crossdomain.xml files enabled numerous cross-site data theft attacks.
Tools to test/exploit
-
Burp Suite — test CORS configurations.
-
CORS Scanner — automated CORS misconfiguration scanner.
-
Browser DevTools — inspect CORS headers.
-
curl — manually test CORS with Origin header.
CVE Examples
-
CVE-2021-38112 — Amazon Kindle CORS vulnerability.
-
CVE-2020-5741 — Plex CORS misconfiguration.
-
CVE-2019-9787 — WordPress CORS issue.
References
-
MITRE. "CWE-942: Permissive Cross-domain Policy with Untrusted Domains." https://cwe.mitre.org/data/definitions/942.html
-
OWASP. "CORS Security." https://owasp.org/www-community/attacks/CORS_OriginHeaderScrutiny