CORS Configuration skill for Codex
Fix the CORS error without opening the API. Codex reproduces the failing request with curl, maps the browser's CORS error to its real cause, often an error page or auth middleware answering the preflight, and fixes it in a single layer. It configures exact origin allowlists in Express, FastAPI, Django, Spring, ASP.NET Core or nginx, and rejects reflected origins, null and unanchored regexes when credentials are allowed.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- cors-configuration Installs to ~/.agents/skills/cors-configuration/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install cors-configuration The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project cors-configuration Installs to .agents/skills/cors-configuration/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/cors-configuration.zipunzip -o cors-configuration.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) cors-configuration PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $cors-configuration, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Configures and debugs Cross-Origin Resource Sharing correctly - exact origin allowlists, credentialed requests, preflight OPTIONS handling, Vary Origin, exposed headers and max-age - in Express, FastAPI, Django, Spring, ASP.NET Core, Go and nginx, and finds dangerous patterns such as reflected origins with credentials, null origin and unanchored regexes. Use when a browser shows a CORS error, when an API must be called from another domain, or when reviewing an Access-Control-Allow-Origin setup for security.
Use it when
- A browser shows a CORS or preflight error
- An API must be called from a different origin, with or without cookies
- Reviewing Access-Control-Allow-* headers for security issues
- Moving CORS between an app and a proxy or API gateway
Use something else when
- Requests are forged cross-site rather than read: use csrf-protection
- You need API gateway routing and auth design overall: use api-gateway-patterns
How to check the result
A curl loop over allowed, unknown, lookalike and null origins returns Access-Control-Allow-Origin only for the allowed one, and the real frontend works in a browser including 401 responses.
The mistake it prevents
Reflecting the request's Origin header back with Access-Control-Allow-Credentials: true, which lets any website read authenticated responses.
Done well means: only the origins that need to read responses can, credentials are allowed only for exact, trusted origins, preflights succeed fast, and every CORS error is diagnosed from the actual request and response headers instead of by loosening the policy until it works.
CORS relaxes the same-origin policy for reading responses. It is not an access control for the server: requests still arrive and side effects still happen. Authentication and CSRF protection stay necessary.
Diagnose before changing
- Read the exact browser console message and the failing request in DevTools Network (enable “Show CORS errors”/preflight rows). Note: origin, method, request headers, whether credentials are sent, and the status of the OPTIONS and actual requests.
- Reproduce with curl so you see what the server really returns:
# Preflight
curl -si -X OPTIONS https://api.localhost:8443/v1/orders \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type, authorization" | sed -n '1,25p'
# Actual request
curl -si https://api.localhost:8443/v1/orders -H "Origin: https://app.example.com" | grep -i "^access-control\|^vary\|^HTTP"
- Find where CORS is configured; two layers (app and proxy/API gateway) adding headers
produce duplicate
Access-Control-Allow-Originvalues, which browsers reject.
rg -n -i "access-control-allow|cors|CORSMiddleware|CorsConfiguration|AllowedOrigins|add_header.*Access-Control" -g '!node_modules'
Error to cause
| Console says | Usual cause | Fix |
|---|---|---|
No Access-Control-Allow-Origin header | Origin not in allowlist, or the response is an error page (401/404/500) from a layer that adds no CORS headers | Add origin; make error responses carry CORS headers too |
| Response to preflight does not have HTTP ok status | OPTIONS hits auth middleware or a 404/405 route | Handle OPTIONS before auth; return 204 |
Wildcard * cannot be used when credentials mode is include | credentials: "include" with Allow-Origin: * | Echo the specific allowed origin and Allow-Credentials: true |
| Header X is not allowed by Access-Control-Allow-Headers | Custom header or Content-Type: application/json not listed | Add to allowed headers |
| Method PUT is not allowed | Missing from Access-Control-Allow-Methods | Add it |
Multiple values a, b in Allow-Origin | Two layers set the header | Configure CORS in one layer |
| Works in curl, fails in browser | curl ignores CORS | Debug via preflight curl above |
| JS cannot read a response header | Not listed in Access-Control-Expose-Headers | Expose it (e.g. Content-Disposition, X-Request-Id) |
Preflight rules
A browser sends a preflight OPTIONS when the request is not “simple”: any method other
than GET/HEAD/POST, any non-safelisted header (including Authorization), or a
Content-Type other than application/x-www-form-urlencoded, multipart/form-data or
text/plain. So every JSON API call from another origin is preflighted.
- Respond to preflight with 204 and no auth requirement (browsers never send credentials on preflight).
Access-Control-Max-Age: 7200caches the preflight; Chromium caps at 2 hours, Firefox at 24 hours.- Always send
Vary: Originwhen the allowed origin depends on the request, or a CDN will serve one origin’s CORS headers to another.
Secure policy rules
| Rule | Why |
|---|---|
| Exact origin allowlist from configuration | Origins are compared as full strings: scheme, host, port |
Never reflect the Origin header without checking it | Reflection plus Allow-Credentials: true lets any site read authenticated data |
Never allow null | Sandboxed iframes and file:// pages send Origin: null; attackers can too |
| Anchor and escape regexes | /example\.com/ matches example.com.evil.net and evilexample.com |
* only for truly public, unauthenticated data | And never with credentials |
| Separate dev origins by environment | http://localhost:5173 should not be in the production list |
| Do not trust all subdomains blindly | One XSS or takeover on any subdomain then reads your API |
Correct origin check:
ALLOWED = {"https://app.example.com", "https://admin.example.com"}
def cors_headers(origin: str | None) -> dict[str, str]:
h = {"Vary": "Origin"}
if origin in ALLOWED:
h["Access-Control-Allow-Origin"] = origin
h["Access-Control-Allow-Credentials"] = "true"
return h
If you must allow a pattern, parse the origin and compare the hostname:
from urllib.parse import urlsplit
def allowed(origin: str) -> bool:
u = urlsplit(origin)
return u.scheme == "https" and u.port in (None, 443) and (
u.hostname == "example.com" or (u.hostname or "").endswith(".example.com"))
Framework configuration
Express (cors package):
import cors from "cors";
const allowed = new Set(process.env.CORS_ORIGINS.split(","));
app.use(cors({
origin: (origin, cb) => cb(null, !origin || allowed.has(origin)), // !origin: same-origin or non-browser
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
exposedHeaders: ["X-Request-Id"],
maxAge: 7200,
}));
origin: true reflects any origin; never combine it with credentials: true.
FastAPI / Starlette:
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=7200,
)
allow_origin_regex is matched with fullmatch, but still escape dots.
Django: django-cors-headers with CORS_ALLOWED_ORIGINS (exact list) and
CORS_ALLOW_CREDENTIALS = True; avoid CORS_ALLOW_ALL_ORIGINS = True outside local
development. Middleware must sit above CommonMiddleware.
Spring: a CorsConfigurationSource bean with setAllowedOrigins(List.of(...)) and
setAllowCredentials(true), plus http.cors(Customizer.withDefaults()) so Spring
Security handles preflight before authentication. setAllowedOriginPatterns("*") with
credentials is reflection in disguise.
ASP.NET Core: builder.Services.AddCors(o => o.AddPolicy("web", p => p.WithOrigins(...).AllowCredentials().WithHeaders(...))),
and app.UseCors("web") between UseRouting and UseAuthorization.
Go: github.com/rs/cors with AllowedOrigins and AllowCredentials; avoid
AllowOriginFunc returning true.
nginx: use a map $http_origin $cors_origin { ... } with exact strings, emit headers
with always, and answer OPTIONS with return 204;. Configure CORS in either nginx or
the app, not both.
Workflow
- [ ] 1. Capture failing request details and reproduce with curl
- [ ] 2. Locate every layer that sets Access-Control-* headers
- [ ] 3. Fix the actual cause from the table, in one layer
- [ ] 4. Review for reflection, null, regex and wildcard-with-credentials issues
- [ ] 5. Add Vary: Origin; make error responses carry CORS headers
- [ ] 6. Verify with curl for an allowed origin, a denied origin and a lookalike origin
Verification
for o in https://app.example.com https://evil.example https://app.example.com.evil.example null; do
printf '%-45s ' "$o"
curl -s -o /dev/null -D - https://api.localhost:8443/v1/me -H "Origin: $o" | grep -i "^access-control-allow-origin" || echo "(none)"
done
Only the allowed origin may appear in the output. Then test the real frontend in a browser, including a request that returns 401 and one that uploads a file.
Output template
## CORS: <api>
Layer owning CORS: <app | proxy | gateway>
Allowed origins (prod): <list> Credentials: <yes/no> Max-Age: <s>
| Check | Result |
|---|---|
| Reflected arbitrary origin | no |
| null origin allowed | no |
| Lookalike origin allowed | no |
| Vary: Origin present | yes |
Root cause of reported error: <one line>
Gotchas
- Fixing CORS for a third-party API you do not control is impossible from the browser; call it from your backend instead.
- A redirect during preflight fails; make the API URL exact (trailing slash, http vs https).
- Chrome’s Private/Local Network Access checks add extra prompts or preflight headers for requests from public sites to private IPs or localhost; this is separate from CORS.
Access-Control-Allow-Credentialsonly matters with cookies or HTTP auth; bearer tokens in headers need allowed headers, not credentials mode.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "CORS Configuration"
short_description: "Fix the CORS error without opening the API"
default_prompt: "$cors-configuration Fix this CORS error: preflight response does not have HTTP ok status" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.