From grimoire
Configures CORS headers with explicit origin allowlists based on OWASP and W3C best practices. Prevents cross-origin data theft for APIs and web services called from browser JavaScript.
How this skill is triggered — by the user, by Claude, or both
Slash command
/grimoire:apply-cors-policyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Configure CORS headers with an explicit allowlist of trusted origins — never reflecting arbitrary origins or using `Access-Control-Allow-Origin: *` with credentials — to prevent cross-origin data theft.
Configure CORS headers with an explicit allowlist of trusted origins — never reflecting arbitrary origins or using Access-Control-Allow-Origin: * with credentials — to prevent cross-origin data theft.
Adopted by: W3C CORS specification is implemented in all modern browsers. OWASP Top 10 2021 A01 (Broken Access Control) includes CORS misconfiguration. AWS API Gateway, Azure APIM, and Cloudflare Workers all provide explicit CORS configuration with origin allowlisting. Major APIs (Stripe, Twilio, SendGrid) require explicit CORS origin configuration.
Impact: CWE-942 (Overly Permissive Cross-origin Policy) is one of the most common API security misconfigurations. A misconfigured CORS that reflects any origin with credentials enables cross-origin attacks: any website can make authenticated requests to your API and read the response. Portswigger Web Security Academy documents CORS misconfigurations leading to account takeover in real-world bug bounty reports. Netflix, Coinbase, and numerous fintechs have paid bug bounties for CORS misconfigurations exposing authenticated API data.
Why best: The browser's same-origin policy already blocks cross-origin reads — CORS is the controlled exception. Access-Control-Allow-Origin: * is safe for public, unauthenticated APIs (public CDN assets, public data APIs) but catastrophic for authenticated APIs. The distinction: * cannot be combined with Access-Control-Allow-Credentials: true in the spec, but reflection attacks bypass this by echoing back the requesting origin instead of using *.
Sources: OWASP CORS Cheat Sheet; W3C CORS spec (fetch.spec.whatwg.org); CWE-942; Portswigger CORS research
Define an explicit allowlist of permitted origins:
ALLOWED_ORIGINS = {
'https://app.example.com',
'https://admin.example.com',
'https://partner.example.com',
}
def get_cors_origin(request_origin):
if request_origin in ALLOWED_ORIGINS:
return request_origin
return None # do not set header for disallowed origins
Set CORS headers on every response, not just the preflight:
from flask import request, make_response
@app.after_request
def add_cors_headers(response):
origin = request.headers.get('Origin')
allowed = get_cors_origin(origin)
if allowed:
response.headers['Access-Control-Allow-Origin'] = allowed
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Vary'] = 'Origin' # required when echoing origin
return response
@app.route('/api/resource', methods=['OPTIONS'])
def preflight():
response = make_response()
origin = request.headers.get('Origin')
allowed = get_cors_origin(origin)
if allowed:
response.headers['Access-Control-Allow-Origin'] = allowed
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
response.headers['Access-Control-Max-Age'] = '86400'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response, 204
Use Access-Control-Allow-Origin: * only for public, unauthenticated endpoints:
# Safe: public CDN assets, open data API (no cookies/auth)
Access-Control-Allow-Origin: *
# Never combine * with credentials:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true ← browsers block this per spec, but see reflection attack
Always set Vary: Origin when reflecting origins dynamically — without this, CDNs and proxies cache one origin's CORS response and serve it to others:
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Restrict allowed headers and methods to what your API actually uses:
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: Content-Type, Authorization, X-CSRF-Token
Do not use Access-Control-Allow-Headers: * for credentialed requests (unsupported in Safari and creates unnecessary exposure).
For development environments — use environment variables for allowed origins so local development doesn't bleed into production:
# development: allow localhost
# production: allow only production domains
ALLOWED_ORIGINS = set(os.environ.get('CORS_ALLOWED_ORIGINS', '').split(','))
Use framework middleware rather than manual headers:
# Flask-CORS
from flask_cors import CORS
CORS(app, origins=['https://app.example.com'], supports_credentials=True)
// Express cors package
const cors = require('cors');
app.use(cors({
origin: ['https://app.example.com'],
credentials: true,
}));
// Next.js App Router — app/api/route.ts
const ALLOWED_ORIGINS = ['https://app.example.com'];
export async function OPTIONS(request: Request) {
const origin = request.headers.get('origin') ?? '';
const allowed = ALLOWED_ORIGINS.includes(origin) ? origin : '';
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': allowed,
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin',
},
});
}
// Go net/http — manual middleware
var allowedOrigins = map[string]bool{
"https://app.example.com": true,
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
w.WriteHeader(http.StatusNoContent)
return
}
}
next.ServeHTTP(w, r)
})
}
# Nginx — origin allowlist via map
map $http_origin $cors_origin {
default "";
"https://app.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Methods "GET, POST" always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Vary Origin always;
proxy_pass http://backend;
}
}
# AWS API Gateway — REST API (serverless.yml / SAM)
Cors:
AllowOrigins:
- https://app.example.com
AllowHeaders:
- Content-Type
- Authorization
AllowMethods:
- GET
- POST
AllowCredentials: true
Origin without checking against an allowlist — if (origin) response.set('Access-Control-Allow-Origin', origin) is the CORS misconfiguration pattern.Access-Control-Allow-Origin: null is dangerous — sandbox iframes and local files send Origin: null, which null allows.Origin header without validation — origin = request.headers['Origin']; response['ACAO'] = origin allows any attacker site to call the API with credentials.Vary: Origin — a cached CORS response for https://app.example.com gets served to https://attacker.com if Vary is missing.if 'example.com' in origin allows https://evil-example.com through.npx claudepluginhub jeffreytse/grimoire --plugin grimoireValidates CORS configurations in web apps/APIs for security misconfigurations like wildcard origins, origin reflection, permissive methods/headers.
Verifies CORS policy enforcement by testing origin reflection, null origin, subdomain bypass, wildcard-with-credentials, and preflight correctness using targeted curl and browser tests.
Audits PHP CORS configurations for security issues like wildcard origins, credentials with wildcards, dynamic origin reflection, missing preflight handling, and overly permissive policies.