Skip to main content

Command Palette

Search for a command to run...

ASP.NET Core CORS Policy: Named vs Default vs Endpoint-Level β€” Enterprise Decision Guide

Updated
β€’9 min read
ASP.NET Core CORS Policy: Named vs Default vs Endpoint-Level β€” Enterprise Decision Guide

Cross-Origin Resource Sharing (CORS) is one of those topics that looks simple until you hit production. Every ASP.NET Core team eventually faces the same set of decisions: do we configure CORS globally or per-endpoint? Should we use named policies or a default policy? What's the actual security risk of AllowAnyOrigin? And why does the order of middleware matter so much?

🎁 Want implementation-ready .NET source code? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. πŸ‘‰ Join CodingDroplets on Patreon

πŸ’» Full Source Code on GitHub: ASP.NET Core CORS Policy Demo

This guide cuts through the confusion and gives enterprise .NET teams a clear framework for making CORS decisions that are secure, maintainable, and production-ready.

The Three CORS Configuration Approaches

ASP.NET Core provides three distinct ways to apply CORS policies, and understanding the difference between them is the foundation of every other decision in this guide.

Named Policies are defined once in your service registration with a unique string identifier and applied selectively β€” either on controllers, action methods, or via the middleware pipeline. This approach gives you maximum flexibility: you can define a strict policy for your public API and a more permissive one for internal services, and switch between them at any layer.

Default Policy is a convenience shortcut β€” you configure a single policy without giving it a name, and CORS middleware automatically applies it everywhere UseCors() is placed. This works well when every endpoint in your application has identical CORS requirements, which is more common in simple APIs than in enterprise systems.

Endpoint-Level CORS via RequireCors() in minimal APIs or [EnableCors] / [DisableCors] attributes in controller-based APIs lets you override the global policy at the finest level of granularity. This is the most surgical approach and is essential when different routes within the same API have genuinely different trust boundaries.


When to Use Each Approach

The right choice depends on how varied your CORS requirements are across your API surface.

Use Named Policies when your application serves multiple client types β€” for example, a public-facing mobile app and an internal admin dashboard that run on different origins. Named policies let you define both in one place and reference them where appropriate, keeping your CORS logic centralized and auditable.

Use the Default Policy only when all your endpoints share identical CORS requirements and you want to minimize configuration boilerplate. This is reasonable for internal microservices behind an API gateway where the gateway handles CORS and the individual services just need a permissive policy for internal calls.

Use Endpoint-Level overrides when you have a handful of endpoints that genuinely need different behavior from the rest β€” for example, a webhook receiver that must accept requests from a third-party domain, while the rest of your API only accepts your own frontend origin. Trying to handle this exclusively at the global level leads to either over-permissive policies or complex conditional logic.


The Security Risks of AllowAnyOrigin

AllowAnyOrigin() is the most misused configuration in CORS. It disables the browser's same-origin protection entirely for your API, meaning any website on the internet can make cross-origin requests to your endpoints from a user's browser.

The critical combination to avoid is AllowAnyOrigin() paired with AllowCredentials(). ASP.NET Core will actually throw an exception if you try this, because the combination is fundamentally insecure β€” it would allow any origin to make authenticated requests using the user's cookies or tokens. The browser blocks credentialed cross-origin requests unless the server explicitly approves the exact requesting origin, which AllowAnyOrigin() cannot do by definition.

Even without credentials, AllowAnyOrigin() should be restricted to genuinely public, read-only endpoints β€” public APIs, CDN-served assets, open data feeds. For any endpoint that reads or modifies user-specific data, you need a specific origin allowlist.

The safer alternative for "open" APIs is to explicitly list the allowed origins, even if that list is long. An explicit allowlist makes your security posture reviewable and auditable in a way that AllowAnyOrigin() never is.


Middleware Ordering β€” Why Placement Matters

CORS middleware must appear before any middleware that terminates the request pipeline β€” specifically before UseRouting(), UseAuthentication(), UseAuthorization(), and MapControllers(). Getting this order wrong is one of the most common causes of CORS failures that look inexplicable in logs.

The correct order for a typical ASP.NET Core Web API is: UseCors() β†’ UseAuthentication() β†’ UseAuthorization() β†’ MapControllers().

When UseCors() appears after UseAuthorization(), preflight OPTIONS requests β€” which browsers send without credentials β€” get blocked by the authorization middleware before CORS headers are ever added to the response. The browser sees a 401 or 403 with no CORS headers and blocks the actual request, leading to a misleading "No 'Access-Control-Allow-Origin' header is present" error that has nothing to do with your CORS configuration being wrong.

For endpoint routing (the default since .NET 3.0), you can also apply CORS between UseRouting() and UseEndpoints() / MapControllers(), which gives CORS access to routing metadata, enabling endpoint-specific policies to work correctly.


CORS Preflight Requests Explained

Preflight is a browser mechanism, not an ASP.NET Core concept. When a browser detects a "non-simple" cross-origin request β€” one that uses a method other than GET/POST, adds custom headers, or sends a content type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain β€” it sends an HTTP OPTIONS request to the same URL before the actual request.

This preflight asks the server: "Will you accept a request from origin X with method Y and headers Z?" The server responds with the appropriate Access-Control-* headers, and the browser either proceeds with the actual request or blocks it.

For enterprise APIs, this means every PUT, DELETE, PATCH, and any POST with a Content-Type: application/json body triggers a preflight. Your CORS configuration must handle OPTIONS requests correctly, which UseCors() does automatically when placed in the right position in the pipeline.

Preflight responses are cached by the browser based on the Access-Control-Max-Age header. Setting a reasonable max-age (e.g., 86400 seconds for 24 hours) significantly reduces the number of preflight round-trips in production, which can measurably improve API response times for frequently-called endpoints from browser clients.


Decision Matrix

Scenario Recommended Approach Why
Single frontend, all endpoints same origin Default Policy Minimal config, no overhead
Multiple client types (mobile app + admin) Named Policies Different trust levels per client
Public read-only API Named Policy with AllowAnyOrigin() Controlled, auditable, scoped
Webhook receiver from third-party Endpoint-Level RequireCors() Isolated β€” doesn't affect rest of API
Internal microservice (behind API Gateway) Default Policy (permissive) Gateway handles public CORS
Mixed public + authenticated endpoints Named Policies + Endpoint overrides Fine-grained control
BFF (Backend for Frontend) pattern Named Policy per frontend Each BFF client has its own policy

Anti-Patterns to Avoid

1. Wildcard credentials: AllowAnyOrigin().AllowCredentials() This is invalid by the CORS spec and ASP.NET Core will throw at startup. If you need credentials, you must specify exact origins.

2. Commenting out CORS for debugging and forgetting to restore it A surprisingly common production incident. Treat CORS configuration as security-critical code β€” it belongs in code review, not in a TODO comment.

3. Applying [EnableCors] without a matching UseCors() in the middleware pipeline The attribute alone does nothing if the CORS middleware isn't registered and added to the pipeline. This silently fails β€” no error, no CORS headers.

4. Using AllowAnyOrigin() because localhost testing was blocked The correct fix for localhost CORS during development is to add http://localhost:PORT to your allowed origins for the Development environment, not to open your policy to all origins globally.

5. Configuring CORS on the reverse proxy AND in ASP.NET Core Running CORS in both Nginx/YARP and the application layer causes duplicate headers and inconsistent behavior. Pick one layer β€” typically the reverse proxy for production, the application layer for local development and testing.

6. Not testing preflight explicitly Most CORS bugs only surface on preflight. Include explicit OPTIONS request tests in your integration test suite β€” don't rely on happy-path GET/POST tests to validate your CORS configuration.


β˜• Found this guide useful? Buy us a coffee β€” it helps us keep producing free, high-quality .NET content every week.

FAQ

Q: Does CORS apply to server-to-server API calls? No. CORS is enforced by the browser. Server-to-server HTTP calls β€” from your backend to another API β€” are not subject to CORS restrictions. Only browser-originated cross-origin requests are affected.

Q: If I disable CORS in my API, does that make it more secure? Not necessarily. CORS does not prevent direct API access β€” it only instructs browsers to block cross-origin responses. An attacker making direct requests with curl, Postman, or custom code is completely unaffected by CORS headers.

Q: Can I configure different CORS policies per environment? Yes, and it's recommended. A common pattern is to use a permissive named policy in Development that allows localhost origins, and a strict named policy in Production that only allows your deployed frontend URLs. Load the allowed origins from environment-specific configuration, not hardcoded values.

Q: What's the difference between WithOrigins() and AllowAnyOrigin()?WithOrigins("https://example.com") adds your origin to an explicit allowlist. AllowAnyOrigin() sets the Access-Control-Allow-Origin: * header, which means any origin is allowed. The key practical difference is that credentialed requests (with cookies or Authorization headers) only work with explicit origins, never with the wildcard.

Q: Does my CORS policy need to include http:// AND https:// separately? Yes. https://example.com and http://example.com are treated as different origins. If you support both (which you generally should not in production β€” HTTPS only), you need to list both explicitly with WithOrigins("https://example.com", "http://example.com").

Q: How do I handle CORS for a Blazor WebAssembly app calling an ASP.NET Core API? The Blazor WASM app runs in the browser, so its API calls are subject to CORS. Add the Blazor app's published origin (e.g., https://app.example.com) to the API's WithOrigins() list. During development, add https://localhost:PORT to the Development environment policy.

Q: What happens if a CORS preflight fails? The browser blocks the actual request entirely and the user sees a CORS error in the browser console. The actual request is never sent to your server, so no server-side logs will show it β€” which is why CORS issues can be hard to diagnose from server logs alone. Always check browser DevTools Network tab for OPTIONS requests when debugging CORS.

More from this blog

C

Coding Droplets

119 posts