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.

  • Security
  • $cors-configuration
  • 205 lines
  • 2 files
  • ~2,150 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- cors-configuration

Installs to ~/.agents/skills/cors-configuration/ for every project on this machine.

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

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.

~/.agents/skills/cors-configuration/SKILL.md

--- name: cors-configuration description: Configures and debugs Cross-Origin Resource Sharing correctly - exact origin allowlists, credentialed requests, preflight OPTIONS handling, Vary… license: MIT ---

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

  1. 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.
  2. 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"
  1. Find where CORS is configured; two layers (app and proxy/API gateway) adding headers produce duplicate Access-Control-Allow-Origin values, which browsers reject.
rg -n -i "access-control-allow|cors|CORSMiddleware|CorsConfiguration|AllowedOrigins|add_header.*Access-Control" -g '!node_modules'

Error to cause

Console saysUsual causeFix
No Access-Control-Allow-Origin headerOrigin not in allowlist, or the response is an error page (401/404/500) from a layer that adds no CORS headersAdd origin; make error responses carry CORS headers too
Response to preflight does not have HTTP ok statusOPTIONS hits auth middleware or a 404/405 routeHandle OPTIONS before auth; return 204
Wildcard * cannot be used when credentials mode is includecredentials: "include" with Allow-Origin: *Echo the specific allowed origin and Allow-Credentials: true
Header X is not allowed by Access-Control-Allow-HeadersCustom header or Content-Type: application/json not listedAdd to allowed headers
Method PUT is not allowedMissing from Access-Control-Allow-MethodsAdd it
Multiple values a, b in Allow-OriginTwo layers set the headerConfigure CORS in one layer
Works in curl, fails in browsercurl ignores CORSDebug via preflight curl above
JS cannot read a response headerNot listed in Access-Control-Expose-HeadersExpose 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: 7200 caches the preflight; Chromium caps at 2 hours, Firefox at 24 hours.
  • Always send Vary: Origin when the allowed origin depends on the request, or a CDN will serve one origin’s CORS headers to another.

Secure policy rules

RuleWhy
Exact origin allowlist from configurationOrigins are compared as full strings: scheme, host, port
Never reflect the Origin header without checking itReflection plus Allow-Credentials: true lets any site read authenticated data
Never allow nullSandboxed 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 dataAnd never with credentials
Separate dev origins by environmenthttp://localhost:5173 should not be in the production list
Do not trust all subdomains blindlyOne 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-Credentials only 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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem