curl -sI https://your-site.com | grep -iE 'content-security|access-control|cross-origin|referrer-policy|permissions-policy' to audit all browser security headers at once.Access-Control-Allow-Origin: * with credentials -- browsers silently reject this, causing hard-to-debug CORS failures.Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true -- browsers reject this combinationsame-origin breaks OAuth popup flows and payment integrations -- use same-origin-allow-popups for those cases| # | Policy/Header | Purpose | Scope | Default Behavior | Bypass Risk |
|---|---|---|---|---|---|
| 1 | Same-Origin Policy (SOP) | Restricts cross-origin DOM access, XHR/fetch reads, and storage | Browser-enforced on all origins | Blocks cross-origin reads; allows embeds and writes | Misconfigured CORS, document.domain (deprecated), postMessage without origin check |
| 2 | Content Security Policy (CSP) | Controls which resources (scripts, styles, images) a page can load | Per-page via HTTP header or <meta> | No restrictions (everything allowed) | unsafe-inline, unsafe-eval, overly broad allowlists, script gadgets |
| 3 | Cross-Origin Resource Sharing (CORS) | Allows servers to opt in to cross-origin requests | Per-resource via response headers | Browsers block cross-origin fetch/XHR responses | Wildcard origin with credentials, reflected Origin header, null origin trust |
| 4 | Cross-Origin-Opener-Policy (COOP) | Isolates browsing context group from cross-origin openers | Per-document via response header | unsafe-none (no isolation) | Breaking popup-based OAuth/payment flows with same-origin |
| 5 | Cross-Origin-Embedder-Policy (COEP) | Requires subresources to opt in via CORS/CORP | Per-document via response header | unsafe-none (no restrictions) | Third-party resources without CORP/CORS headers get blocked |
| 6 | Cross-Origin-Resource-Policy (CORP) | Controls which origins can embed a resource | Per-resource via response header | Resources loadable from any origin | Blocks legitimate cross-origin embeds if set too restrictively |
| 7 | Permissions-Policy | Restricts browser features (camera, geolocation, etc.) | Per-page and per-iframe | All features available to top-level | Overly permissive allow on iframes |
| 8 | Referrer-Policy | Controls what referrer info is sent with requests | Per-page or per-request | strict-origin-when-cross-origin (Chrome 85+ default) | no-referrer-when-downgrade leaks full URL on HTTPS-to-HTTPS navigations |
| Component | Same-Origin Example | Cross-Origin Example |
|---|---|---|
| Scheme | https://a.com = https://a.com | http://a.com != https://a.com |
| Host | a.com = a.com | a.com != b.a.com |
| Port | a.com:443 = a.com:443 | a.com:443 != a.com:8443 |
START: What is your cross-origin security goal?
├── Need to BLOCK cross-origin data reads?
│ ├── YES → SOP handles this by default. No action needed.
│ └── NO ↓
├── Need to ALLOW cross-origin API access?
│ ├── YES → Configure CORS headers on the server (Access-Control-Allow-Origin)
│ └── NO ↓
├── Need to prevent inline script injection / restrict resource loading?
│ ├── YES → Deploy Content Security Policy (CSP) with nonce-based script-src
│ └── NO ↓
├── Need SharedArrayBuffer or high-res timers (cross-origin isolation)?
│ ├── YES → Set COOP: same-origin + COEP: require-corp on the document
│ └── NO ↓
├── Need to restrict browser features in embedded iframes?
│ ├── YES → Use Permissions-Policy header + iframe allow attribute
│ └── NO ↓
├── Need to limit referrer information leakage?
│ ├── YES → Set Referrer-Policy: strict-origin-when-cross-origin (or stricter)
│ └── NO ↓
└── DEFAULT → Deploy all security headers: CSP + COOP + COEP + Permissions-Policy + Referrer-Policy
Check which headers your site already sends. Missing headers mean the browser uses permissive defaults. [src1]
# Audit all browser security headers at once
curl -sI https://your-site.com | grep -iE \
'content-security|access-control|cross-origin|referrer-policy|permissions-policy|x-frame|strict-transport'
Verify: Every key header should appear in the output: Content-Security-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Referrer-Policy, Permissions-Policy.
CSP is the most impactful single header -- it mitigates XSS, data injection, and clickjacking. Use nonce-based policies for best protection. [src3]
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{random}' 'strict-dynamic';
style-src 'self' 'nonce-{random}';
img-src 'self' https:;
font-src 'self';
connect-src 'self' https://api.your-domain.com;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
upgrade-insecure-requests;
Verify: Inject <script>alert(1)</script> -- CSP should block it. Check DevTools Console for CSP violation reports.
Set specific origins -- never reflect the Origin header blindly. [src2]
// Node.js/Express CORS configuration
const cors = require('cors'); // ^2.8.0
const corsOptions = {
origin: ['https://your-app.com', 'https://staging.your-app.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // Preflight cache: 24 hours
};
app.use('/api', cors(corsOptions));
Verify: curl -H "Origin: https://evil.com" -sI https://api.your-site.com/endpoint -- should NOT return Access-Control-Allow-Origin: https://evil.com.
Required for SharedArrayBuffer and high-resolution timers. Test in report-only mode first. [src5]
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
For pages needing OAuth popups or payment integrations:
Cross-Origin-Opener-Policy: same-origin-allow-popups
Cross-Origin-Embedder-Policy: credentialless
Verify: Open DevTools Console and check self.crossOriginIsolated === true.
Restrict unnecessary browser features and control referrer leakage. [src1]
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(self),
usb=(), magnetometer=(), gyroscope=(), accelerometer=()
Referrer-Policy: strict-origin-when-cross-origin
Verify: document.featurePolicy.allowsFeature('camera') should return false in DevTools Console.
const crypto = require('crypto');
function securityHeaders(req, res, next) {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.cspNonce = nonce;
res.set({
'Content-Security-Policy':
`default-src 'self'; script-src 'nonce-${nonce}' 'strict-dynamic'; ` +
`style-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'none'; ` +
`frame-ancestors 'none'; form-action 'self'; upgrade-insecure-requests`,
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY'
});
next();
}
# /etc/nginx/snippets/security-headers.conf
add_header Content-Security-Policy
"default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Embedder-Policy "require-corp" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy
"camera=(), microphone=(), geolocation=(), payment=(self)"
always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# settings.py -- Django 4.x+ built-in security middleware
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin"
SECURE_REFERRER_POLICY = "strict-origin-when-cross-origin"
X_FRAME_OPTIONS = "DENY"
# CSP via django-csp (^4.0)
CSP_DEFAULT_SRC = ("'self'",)
CSP_SCRIPT_SRC = ("'self'",)
CSP_OBJECT_SRC = ("'none'",)
CSP_BASE_URI = ("'none'",)
CSP_FRAME_ANCESTORS = ("'none'",)
# CORS via django-cors-headers (^4.0)
CORS_ALLOWED_ORIGINS = [
"https://your-frontend.com",
"https://staging.your-frontend.com",
]
CORS_ALLOW_CREDENTIALS = True
package main
import "net/http"
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy",
"default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}
// BAD -- reflects any origin, equivalent to disabling SOP
app.use((req, res, next) => {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Access-Control-Allow-Credentials', 'true');
next();
});
// Attacker on https://evil.com can read authenticated responses
// GOOD -- only trusted origins can make credentialed requests
const ALLOWED = new Set(['https://app.example.com', 'https://staging.example.com']);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (ALLOWED.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Access-Control-Allow-Credentials', 'true');
res.set('Vary', 'Origin'); // Critical for caching
}
next();
});
# BAD -- defeats the entire purpose of CSP
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'
# GOOD -- nonces block injected scripts, strict-dynamic trusts loader chains
Content-Security-Policy: default-src 'self'; script-src 'nonce-abc123' 'strict-dynamic'
// BAD -- null origin comes from sandboxed iframes, data: URLs, redirects
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin === 'null') {
res.set('Access-Control-Allow-Origin', 'null');
}
next();
});
// Attacker can forge null origin via sandboxed iframe
// GOOD -- reject null and only trust explicit HTTPS origins
const ALLOWED = new Set(['https://app.example.com']);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.set('Access-Control-Allow-Origin', origin);
res.set('Vary', 'Origin');
}
next();
});
// BAD -- CDN caches response for one origin, serves it to all
res.set('Access-Control-Allow-Origin', req.headers.origin);
// Missing: res.set('Vary', 'Origin');
// GOOD -- ensures CDN/proxy caches differentiate by origin
res.set('Access-Control-Allow-Origin', origin);
res.set('Vary', 'Origin');
Access-Control-Max-Age. If you change allowed methods/headers, users won't see the change until the cache expires. Fix: Set a reasonable Access-Control-Max-Age (e.g., 86400 seconds) and document it. [src2]strict-dynamic trusts any script loaded by a trusted script. Libraries like jQuery or Angular that evaluate strings as code can be exploited as script gadgets. Fix: Audit third-party libraries for eval()-like behavior; prefer nonce-only policies. [src6]Cross-Origin-Embedder-Policy: require-corp blocks all cross-origin resources without CORP/CORS headers (ads, analytics, fonts). Fix: Use credentialless mode or add crossorigin attribute to <img>, <script>, <link> tags. [src5]window.addEventListener('message', handler) without checking event.origin enables cross-origin data injection. Fix: Always validate event.origin against an allowlist before processing the message. [src1]Cross-Origin-Opener-Policy: same-origin severs the window.opener reference for cross-origin popups, breaking OAuth redirect flows. Fix: Use same-origin-allow-popups when popup communication is needed. [src5]strict-origin-when-cross-origin stops sending referrer on protocol downgrade, but no-referrer-when-downgrade leaks the full URL on HTTPS-to-HTTPS cross-origin requests. Fix: Use strict-origin-when-cross-origin or no-referrer. [src1]document.domain to relax SOP between subdomains is deprecated in Chrome 115+ and disabled by default. Fix: Use postMessage() or CORS for cross-subdomain communication. [src1]# Audit all security headers at once
curl -sI https://your-site.com | grep -iE \
'content-security|access-control|cross-origin|referrer-policy|permissions-policy|x-frame|strict-transport'
# Test CORS: check if an origin is allowed
curl -H "Origin: https://app.example.com" -sI https://api.example.com/endpoint \
| grep -i access-control
# Test CORS preflight (OPTIONS request)
curl -X OPTIONS \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type, Authorization" \
-sI https://api.example.com/endpoint
# Check cross-origin isolation status (in browser console)
# self.crossOriginIsolated // should be true
# Validate CSP: visit https://csp-evaluator.withgoogle.com/
# Scan security headers: visit https://securityheaders.com/?q=https://your-site.com
# Test for CORS misconfiguration (evil origin should be rejected)
curl -H "Origin: https://evil.com" -sI https://your-site.com/api \
| grep -i access-control-allow-origin
| Standard/Feature | Status | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|---|
| Same-Origin Policy (SOP) | Foundational | All | All | All | All |
| CSP Level 2 | W3C Rec | 40+ | 31+ | 10+ | 15+ |
| CSP Level 3 (strict-dynamic, nonces) | W3C WD | 59+ | 58+ | 15.4+ | 79+ |
| CORS (basic) | W3C Rec | 4+ | 3.5+ | 4+ | 12+ |
| COOP (same-origin) | Standard | 83+ | 79+ | 15.2+ | 83+ |
| COEP (require-corp) | Standard | 83+ | 79+ | 15.2+ | 83+ |
| COEP (credentialless) | Standard | 96+ | 119+ | No | 96+ |
| Cross-Origin-Resource-Policy (CORP) | Standard | 73+ | 74+ | 12+ | 79+ |
| Permissions-Policy | Standard | 88+ | 65+ (partial) | No | 88+ |
| Referrer-Policy | Standard | 56+ | 50+ | 11.1+ | 79+ |
| Site Isolation | Chrome-specific | 67+ (desktop) | Fission (95+) | Process per tab | 67+ |
| Trusted Types | Draft | 83+ | Behind flag | No | 83+ |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Your API serves multiple frontend origins | API and frontend share the same origin | SOP already blocks unauthorized access -- CORS not needed |
| Preventing inline script injection (XSS defense layer) | You need sole XSS prevention | CSP alone is insufficient -- add output encoding (see XSS Prevention unit) |
| App uses SharedArrayBuffer, WASM threads, or high-res timers | App is a simple content site with no advanced APIs | COOP/COEP add complexity with no benefit |
| Embedding third-party iframes that should not access camera/mic | Top-level page controls all features itself | Permissions-Policy is most valuable for iframe restrictions |
| Hosting resources that should only load on your own site | Hosting public CDN resources meant for cross-origin use | Set CORP: cross-origin for public resources |
strict-dynamic propagates trust: if a trusted script loads a third-party script, that third-party script is also trusted, which can be exploited via script gadgets in libraries like jQuery or Angulardocument.domain is deprecated and disabled by default in Chrome 115+ -- do not rely on it for cross-subdomain communication<iframe allow="..."> for broader compatibility)