Origin Validation Error
Description
Origin Validation Error occurs when software does not properly verify the origin of a request, message, or data. This allows attackers from untrusted origins to interact with the application as if they were from a trusted source. Common manifestations include improper CORS configuration, missing or incorrect Origin header validation, trusting the Referer header for security decisions, and accepting WebSocket connections from any origin.
Risk
Origin validation failures enable cross-site attacks that bypass same-origin policy protections. Attackers can make authenticated requests from malicious websites, steal data through CORS misconfigurations, or establish WebSocket connections to hijack real-time communications. The risk is amplified when combined with credentials—reflected origins with credentials enabled is particularly dangerous. API endpoints with broken origin validation expose all authenticated users to cross-site attacks.
Solution
Implement strict origin validation using allowlists. Never reflect arbitrary Origin headers when credentials are involved. Use the Origin header (not Referer) for security decisions. Validate WebSocket connection origins. Implement proper CORS policies with specific allowed origins. Use SameSite cookies as additional protection. For sensitive operations, require additional verification beyond origin checking. Validate the entire origin including scheme and port.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Data Theft Attackers can read responses from cross-origin requests, stealing sensitive data. |
| Integrity | Scope: Unauthorized Actions CSRF-like attacks can modify data using the victim's credentials. |
| Authentication | Scope: Session Abuse Authenticated sessions are abused from attacker-controlled websites. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Reflecting any origin
from flask import Flask, request
@app.after_request
def add_cors_headers(response):
# Reflects ANY origin - dangerous with credentials!
origin = request.headers.get('Origin')
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
# VULNERABLE: Using Referer for origin validation
@app.route('/api/transfer', methods=['POST'])
def transfer_funds():
referer = request.headers.get('Referer')
# Referer can be spoofed or omitted!
if referer and 'example.com' in referer:
# Process transfer...
return jsonify({'status': 'success'})
return jsonify({'error': 'Invalid request'}), 403
# VULNERABLE: Partial origin matching
@app.after_request
def weak_origin_check(response):
origin = request.headers.get('Origin', '')
# Attacker uses: evil-example.com or example.com.evil.com
if 'example.com' in origin:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
# VULNERABLE: WebSocket without origin check
from flask_socketio import SocketIO
socketio = SocketIO(app, cors_allowed_origins='*') # Allows ANY origin!
@socketio.on('connect')
def handle_connect():
# No origin validation!
pass
// VULNERABLE: Reflecting origin in Java
@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;
// Reflects any origin!
String origin = request.getHeader("Origin");
response.setHeader("Access-Control-Allow-Origin", origin);
response.setHeader("Access-Control-Allow-Credentials", "true");
chain.doFilter(req, res);
}
}
// VULNERABLE: Regex bypass
@Component
public class WeakOriginValidator {
private static final Pattern ORIGIN_PATTERN = Pattern.compile(".*\\.example\\.com");
public boolean isValidOrigin(String origin) {
// Matches: evil.example.com but also evilexample.com
return origin != null && ORIGIN_PATTERN.matcher(origin).matches();
}
}
// VULNERABLE: Spring CORS with wildcard and credentials
@Configuration
public class VulnerableCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*") // Any origin!
.allowCredentials(true); // With credentials!
}
}
// VULNERABLE: Express with reflected origin
app.use((req, res, next) => {
// Reflects any origin!
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header('Access-Control-Allow-Credentials', 'true');
next();
});
// VULNERABLE: WebSocket without origin validation
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
// No origin check!
ws.on('message', (message) => {
// Process message...
});
});
// VULNERABLE: PostMessage without origin validation
window.addEventListener('message', (event) => {
// Accepts messages from ANY origin!
const data = event.data;
processData(data);
});
// VULNERABLE: Substring origin check
function checkOrigin(origin) {
// Attacker uses: https://example.com.evil.com
return origin && origin.includes('example.com');
}
Fixed Code
# SAFE: Strict origin allowlist
from flask import Flask, request
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://www.example.com',
'https://admin.example.com'
}
@app.after_request
def add_cors_headers_safe(response):
origin = request.headers.get('Origin')
if 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'
return response
# SAFE: Proper origin validation
from urllib.parse import urlparse
def is_valid_origin(origin):
"""Validate origin against strict allowlist."""
if not origin:
return False
try:
parsed = urlparse(origin)
# Must be HTTPS
if parsed.scheme != 'https':
return False
# Exact domain match (including port if present)
allowed_hosts = {'app.example.com', 'www.example.com'}
return parsed.netloc in allowed_hosts
except Exception:
return False
@app.route('/api/data')
def get_data():
origin = request.headers.get('Origin')
if not is_valid_origin(origin):
return jsonify({'error': 'Invalid origin'}), 403
# Process request...
return jsonify({'data': 'sensitive'})
# SAFE: WebSocket with origin validation
from flask_socketio import SocketIO, disconnect
ALLOWED_WS_ORIGINS = ['https://app.example.com', 'https://www.example.com']
socketio = SocketIO(app, cors_allowed_origins=ALLOWED_WS_ORIGINS)
@socketio.on('connect')
def handle_connect():
# Additional validation if needed
origin = request.headers.get('Origin')
if origin not in ALLOWED_WS_ORIGINS:
disconnect()
return False
# SAFE: Subdomain validation with proper checks
def is_valid_subdomain(origin):
"""Validate that origin is a valid subdomain."""
if not origin:
return False
try:
parsed = urlparse(origin)
if parsed.scheme != 'https':
return False
host = parsed.netloc.lower()
# Must end with .example.com or be exactly example.com
if host == 'example.com':
return True
if host.endswith('.example.com'):
# Ensure it's a proper subdomain, not evil-example.com
subdomain = host[:-len('.example.com')]
# Validate subdomain format
if subdomain and '.' not in subdomain and subdomain.isalnum():
return True
return False
except Exception:
return False
// SAFE: Java with strict origin validation
@WebFilter("/*")
public class SecureCorsFilter implements Filter {
private static final Set<String> ALLOWED_ORIGINS = Set.of(
"https://app.example.com",
"https://www.example.com",
"https://admin.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: Spring Security CORS configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Specific origins only
configuration.setAllowedOrigins(List.of(
"https://app.example.com",
"https://www.example.com"
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
// SAFE: WebSocket origin validation
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins(
"https://app.example.com",
"https://www.example.com"
)
.withSockJS();
}
}
// SAFE: Express with strict origin validation
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://www.example.com',
'https://admin.example.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED_ORIGINS.has(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();
});
// SAFE: WebSocket with origin check
const WebSocket = require('ws');
const ALLOWED_WS_ORIGINS = new Set([
'https://app.example.com',
'https://www.example.com'
]);
const wss = new WebSocket.Server({
port: 8080,
verifyClient: (info, callback) => {
const origin = info.origin;
if (ALLOWED_WS_ORIGINS.has(origin)) {
callback(true);
} else {
callback(false, 403, 'Origin not allowed');
}
}
});
// SAFE: PostMessage with origin validation
window.addEventListener('message', (event) => {
// Validate origin
if (event.origin !== 'https://trusted.example.com') {
console.warn('Message from untrusted origin:', event.origin);
return;
}
// Safe to process
processData(event.data);
});
// SAFE: Helper function for subdomain validation
function isValidOrigin(origin) {
if (!origin) return false;
try {
const url = new URL(origin);
// Must be HTTPS
if (url.protocol !== 'https:') return false;
const hostname = url.hostname.toLowerCase();
// Exact match or valid subdomain
if (hostname === 'example.com') return true;
if (hostname.endsWith('.example.com')) {
// Get subdomain
const subdomain = hostname.slice(0, -('.example.com'.length));
// Validate: single label, alphanumeric + hyphen
return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(subdomain);
}
return false;
} catch {
return false;
}
}
Exploited in the Wild
Facebook CORS Vulnerability
A CORS misconfiguration allowed attackers to read private data from Facebook APIs when users visited malicious sites.
Multiple API Breaches
Many organizations have suffered data breaches due to reflecting arbitrary origins with credentials, allowing any website to steal user data.
WebSocket Hijacking
Applications accepting WebSocket connections from any origin have been exploited to hijack real-time communications.
Tools to test/exploit
-
Burp Suite — test CORS configurations.
-
CORScanner — automated CORS misconfiguration scanner.
-
Browser DevTools — inspect CORS headers.
-
Postman — test API with various Origin headers.
CVE Examples
-
CVE-2021-22893 — Pulse Secure CORS vulnerability.
-
CVE-2019-9787 — WordPress origin validation bypass.
-
CVE-2020-5741 — Plex CORS misconfiguration.
References
-
MITRE. "CWE-346: Origin Validation Error." https://cwe.mitre.org/data/definitions/346.html
-
OWASP. "CORS Security." https://owasp.org/www-community/attacks/CORS_OriginHeaderScrutiny