skills/security/cors-configuration/SKILL.md
Use when configuring CORS policies for .NET APIs.
npx skillsauth add faysilalshareef/dotnet-ai-kit cors-configurationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
AllowAnyOrigin() disables the browser's same-origin protectionAccess-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true — browsers will block the responseUseCors() must appear after UseRouting() and before UseAuthentication() / UseAuthorization():5173, API on :5000)// Program.cs
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization");
});
});
var app = builder.Build();
app.UseRouting();
app.UseCors(); // after UseRouting, before UseAuth
app.UseAuthentication();
app.UseAuthorization();
Use named policies when different endpoint groups need different CORS rules.
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSPA", policy =>
policy
.WithOrigins(
"https://app.example.com",
"https://staging.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders(
"Content-Type",
"Authorization",
"X-Request-Id"));
options.AddPolicy("AllowReporting", policy =>
policy
.WithOrigins("https://reports.example.com")
.WithMethods("GET")
.WithHeaders("Authorization"));
options.AddPolicy("AllowPublic", policy =>
policy
.AllowAnyOrigin()
.WithMethods("GET")
.WithHeaders("Content-Type"));
});
When your SPA sends cookies or Authorization headers with credentials: "include", you must explicitly allow credentials. This is incompatible with AllowAnyOrigin().
options.AddPolicy("AllowSPAWithCredentials", policy =>
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.AllowCredentials());
If you need to support multiple subdomains dynamically while still using credentials, use SetIsOriginAllowed:
options.AddPolicy("AllowSubdomains", policy =>
policy
.SetIsOriginAllowed(origin =>
{
var host = new Uri(origin).Host;
return host == "example.com"
|| host.EndsWith(".example.com",
StringComparison.OrdinalIgnoreCase);
})
.AllowCredentials()
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization"));
Apply named policies to specific endpoint groups instead of globally.
// Minimal API — RequireCors on a group
var api = app.MapGroup("/api")
.RequireCors("AllowSPA");
api.MapGet("/orders", GetOrders);
api.MapPost("/orders", CreateOrder);
// Public health check — different policy
app.MapGet("/health", () => Results.Ok("Healthy"))
.RequireCors("AllowPublic");
With controllers, use the [EnableCors] attribute:
[ApiController]
[Route("api/[controller]")]
[EnableCors("AllowSPA")]
public class OrdersController : ControllerBase
{
// All actions inherit "AllowSPA"
[DisableCors]
[HttpGet("internal-status")]
public IActionResult InternalStatus() => Ok();
}
Browsers send an OPTIONS preflight request before non-simple cross-origin requests. Cache the preflight response to avoid redundant round-trips.
options.AddPolicy("AllowSPA", policy =>
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Content-Type", "Authorization")
.SetPreflightMaxAge(TimeSpan.FromHours(1)));
Use environment-specific CORS configuration so development is convenient while production stays locked down.
if (builder.Environment.IsDevelopment())
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy
.WithOrigins(
"https://localhost:5173",
"http://localhost:5173",
"https://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
}
else
{
var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? [];
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
policy
.WithOrigins(allowedOrigins)
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders(
"Content-Type",
"Authorization",
"X-Request-Id")
.AllowCredentials()
.SetPreflightMaxAge(
TimeSpan.FromHours(1)));
});
}
// appsettings.Production.json
{
"Cors": {
"AllowedOrigins": [
"https://app.example.com",
"https://admin.example.com"
]
}
}
For complex projects, bind CORS configuration to a strongly-typed options class.
public sealed class CorsOptions
{
public const string SectionName = "Cors";
public string[] AllowedOrigins { get; init; } = [];
public string[] AllowedMethods { get; init; } =
["GET", "POST", "PUT", "DELETE"];
public string[] AllowedHeaders { get; init; } =
["Content-Type", "Authorization"];
public bool AllowCredentials { get; init; } = true;
public int PreflightMaxAgeSeconds { get; init; } = 3600;
}
// Registration
var corsConfig = builder.Configuration
.GetSection(CorsOptions.SectionName)
.Get<CorsOptions>() ?? new CorsOptions();
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy
.WithOrigins(corsConfig.AllowedOrigins)
.WithMethods(corsConfig.AllowedMethods)
.WithHeaders(corsConfig.AllowedHeaders)
.SetPreflightMaxAge(
TimeSpan.FromSeconds(
corsConfig.PreflightMaxAgeSeconds));
if (corsConfig.AllowCredentials)
policy.AllowCredentials();
});
});
| Mistake | Problem | Fix |
|---------|---------|-----|
| AllowAnyOrigin() + AllowCredentials() | CORS spec violation — browsers block the response | Use WithOrigins(...) with explicit origins when credentials are needed |
| UseCors() after UseAuthorization() | CORS middleware never runs for unauthorized preflight requests, returning 401 | Place UseCors() before UseAuthentication() and UseAuthorization() |
| Origins with trailing slash | https://app.example.com/ does not match https://app.example.com | Remove trailing slashes from all origin strings |
| Hardcoded localhost origins in production | Opens your API to any local dev machine | Use environment-conditional configuration (see Development vs Production) |
| Missing OPTIONS method in WithMethods() | Not needed — the CORS middleware handles preflight OPTIONS automatically | Do not add OPTIONS to WithMethods(); it is implicit |
| AllowAnyOrigin() on authenticated endpoints | Any website can make authenticated requests on behalf of your users | Restrict to known origins |
| Wildcard headers with AllowAnyHeader() | Exposes your API to unexpected custom headers | List only the headers your clients actually send |
| Not exposing response headers | Client JavaScript cannot read custom response headers unless exposed | Use WithExposedHeaders("X-Pagination", "X-Request-Id") |
| Duplicating CORS headers in both middleware and reverse proxy | Browsers reject responses with duplicate Access-Control-Allow-Origin | Configure CORS in exactly one layer |
| Scenario | Policy Configuration |
|----------|---------------------|
| Single SPA, same domain, different port (dev) | Default policy, WithOrigins("https://localhost:PORT"), AllowCredentials() |
| Single SPA, different domain (prod) | Default policy, WithOrigins("https://app.example.com"), AllowCredentials() |
| Multiple SPAs, different domains | Named policies per SPA group, RequireCors("PolicyName") per endpoint group |
| Public read-only API | AllowAnyOrigin(), WithMethods("GET"), no credentials |
| Subdomain wildcard | SetIsOriginAllowed() with domain suffix check, AllowCredentials() |
| Server-to-server only | No CORS configuration needed |
| API gateway handles CORS | No CORS in .NET — configure at the gateway layer only |
| Mixed public + authenticated endpoints | Named policies: one restrictive with credentials, one permissive without |
| Anti-Pattern | Why It Is Harmful | Better Approach |
|-------------|-------------------|-----------------|
| AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader() | Effectively disables CORS protection entirely | Specify exact origins, methods, and headers |
| SetIsOriginAllowed(_ => true) | Equivalent to AllowAnyOrigin() but bypasses the wildcard-credentials check — worst of both worlds | Validate the origin against an allowlist or domain pattern |
| Copying CORS config from Stack Overflow without reviewing | Most SO answers use AllowAny* for simplicity — not production-safe | Follow least-privilege principle for each policy |
| One global permissive policy for all endpoints | Public endpoints and authenticated endpoints have different threat models | Use named policies and apply per-group |
| Relying on CORS as an authentication mechanism | CORS is browser-enforced only — curl, Postman, and servers ignore it | Always enforce server-side authentication and authorization |
| Adding CORS headers manually via middleware | Bypasses the built-in CORS negotiation logic and is error-prone | Use AddCors() and UseCors() exclusively |
AddCors in Program.cs or Startup.csUseCors in the middleware pipeline[EnableCors] or [DisableCors] attributes on controllersRequireCors on minimal API endpoint groupsAccess-Control-Allow-Origin header manipulationAddCors() with named policies matching your endpoint groupsUseCors() in the correct middleware position (after routing, before auth)RequireCors() or [EnableCors()]SetPreflightMaxAge to reduce OPTIONS trafficAccess-Control-* response headers on preflight and actual requestsdata-ai
Use when about to claim work is complete, fixed, passing, or ready — before committing, creating PRs, or moving to the next task. Requires running verification commands and confirming output before making any success claims.
development
Use when encountering any bug, test failure, build error, or unexpected behavior — before proposing fixes or making changes.
development
Use when checkpointing, wrapping up, or handing off an AI-assisted development session.
development
Use when following the Specification-Driven Development lifecycle from plan through ship.