DEV_NET_CORE
GET_STARTED
Design & ArchitectureAuthentication, authorization, and web security

CORS, secure headers, secret handling, and least privilege

Overview

CORS, secure headers, secret handling, and least privilege are core web security topics that help protect applications from browser-based attacks, data leakage, credential misuse, and excessive access rights.

In a modern full-stack application, security is not handled by one feature only. It is usually built from multiple layers:

  • CORS controls which browser-based frontends are allowed to read responses from an API across origins.
  • Secure HTTP response headers instruct browsers to apply additional protections such as HTTPS enforcement, script restrictions, clickjacking protection, MIME-sniffing prevention, and referrer control.
  • Secret handling protects sensitive values such as connection strings, API keys, signing keys, certificates, and tokens.
  • Least privilege limits what users, services, applications, and infrastructure identities can access.

These topics are important because many production security problems are caused by misconfiguration rather than complex code bugs. Examples include allowing any CORS origin with credentials, missing security headers, committing secrets to source control, giving an application broad database permissions, or assigning a cloud identity access to an entire subscription when it only needs one resource.

For interviews, this topic is important because it tests practical security judgment. A strong candidate should be able to explain not only what CORS or security headers are, but also where they fit in the request pipeline, what they do not protect against, how to configure them safely, how to manage secrets across environments, and how to design access using least privilege.

Core Concepts

Same-Origin Policy and CORS

The same-origin policy is a browser security rule that restricts a script loaded from one origin from reading sensitive data from another origin unless the target server explicitly allows it.

An origin is defined by the combination of:

  • Scheme, such as https
  • Host, such as app.example.com
  • Port, such as 443

For example, these are different origins:

Code
https://app.example.com
https://api.example.com
http://app.example.com
https://app.example.com:5001

CORS stands for Cross-Origin Resource Sharing. It is a browser-enforced mechanism that allows a server to say which origins can read cross-origin responses.

CORS is not authentication. CORS does not prove who the user is. It only controls whether the browser exposes the response to frontend JavaScript.

A common example is a React app calling an ASP.NET Core API:

Code
Frontend: https://app.example.com
API:      https://api.example.com

Because the frontend and API use different hosts, the browser treats them as different origins. The API must return appropriate CORS headers before browser JavaScript can read the response.

Simple Requests and Preflight Requests

Some cross-origin requests are considered simple requests. Others require a preflight request.

A preflight request is an automatic OPTIONS request sent by the browser before the actual request. The browser asks the server whether the cross-origin request is allowed.

A preflight is commonly triggered by:

  • Non-simple HTTP methods such as PUT, PATCH, or DELETE
  • Custom request headers such as Authorization or X-Correlation-Id
  • Certain content types, such as application/json

Example preflight request:

Code
OPTIONS /api/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

Example response:

Code
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers: content-type, authorization

If the preflight response does not allow the origin, method, or headers, the browser blocks the actual request.

Safe CORS Configuration

A safe CORS policy should be as specific as possible.

Good approach:

Code
builder.Services.AddCors(options =>
{
    options.AddPolicy("FrontendApp", policy =>
    {
        policy
            .WithOrigins("https://app.example.com")
            .WithMethods("GET", "POST", "PUT", "DELETE")
            .WithHeaders("Content-Type", "Authorization")
            .AllowCredentials();
    });
});

var app = builder.Build();

app.UseRouting();
app.UseCors("FrontendApp");
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

Risky approach:

Code
builder.Services.AddCors(options =>
{
    options.AddPolicy("Unsafe", policy =>
    {
        policy
            .AllowAnyOrigin()
            .AllowAnyMethod()
            .AllowAnyHeader();
    });
});

AllowAnyOrigin may be acceptable for a public read-only API that does not use cookies, credentials, or sensitive user data. It is usually unsafe for authenticated business APIs.

The most dangerous CORS mistake is allowing any origin together with credentials. Credentialed requests include cookies, client certificates, or HTTP authentication. For credentialed cross-origin requests, the server should allow only trusted origins.

Important CORS habits:

  • Do not use CORS as an authorization mechanism.
  • Do not allow all origins for private APIs.
  • Do not allow credentials unless the frontend really needs them.
  • Keep environment-specific origins in configuration.
  • Configure CORS before authorization in the ASP.NET Core middleware pipeline when endpoint routing is used.
  • Remember that CORS is enforced by browsers, not by every HTTP client.

CORS vs CSRF

CORS and CSRF are related to browser security, but they solve different problems.

CORS controls whether browser JavaScript can read a cross-origin response.

CSRF, or Cross-Site Request Forgery, is an attack where a malicious site causes a user's browser to send a request to another site where the user is already authenticated.

CORS does not automatically prevent CSRF. If an application uses cookies for authentication, the browser may send those cookies automatically depending on cookie settings. Even if CORS blocks the malicious site from reading the response, the state-changing request may still reach the server unless CSRF protection is implemented.

For cookie-based authentication, use protections such as:

  • Anti-forgery tokens
  • SameSite cookies
  • Origin or Referer validation for sensitive operations
  • Safe HTTP semantics, where GET does not change state
  • Explicit re-authentication for high-risk operations

Secure HTTP Response Headers

Secure headers are response headers that tell browsers to apply additional security rules.

They do not replace authentication, authorization, validation, or secure coding. They reduce the impact of common browser-based attacks and misconfigurations.

Common secure headers include:

Code
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Content-Security-Policy: default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'

Important headers:

  • Strict-Transport-Security tells browsers to use HTTPS for future requests.
  • Content-Security-Policy restricts where scripts, styles, images, frames, and other resources can be loaded from.
  • X-Content-Type-Options: nosniff prevents MIME-sniffing attacks.
  • X-Frame-Options helps prevent clickjacking in older browser scenarios.
  • frame-ancestors in CSP is the modern way to control who can embed the page.
  • Referrer-Policy controls how much referrer information is sent to other sites.
  • Permissions-Policy limits browser features such as camera, microphone, geolocation, and payment APIs.
  • Cache-Control can prevent sensitive pages from being stored by browsers or proxies.

Adding Secure Headers in ASP.NET Core

Secure headers can be added using middleware.

Example:

Code
app.Use(async (context, next) =>
{
    context.Response.Headers.TryAdd("X-Content-Type-Options", "nosniff");
    context.Response.Headers.TryAdd("X-Frame-Options", "DENY");
    context.Response.Headers.TryAdd("Referrer-Policy", "strict-origin-when-cross-origin");
    context.Response.Headers.TryAdd("Permissions-Policy", "camera=(), microphone=(), geolocation=()");

    context.Response.Headers.TryAdd(
        "Content-Security-Policy",
        "default-src 'self'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'");

    await next();
});

HSTS is commonly configured separately:

Code
if (!app.Environment.IsDevelopment())
{
    app.UseHsts();
}

app.UseHttpsRedirection();

Security headers should be tested carefully. For example, a strict CSP can break scripts, styles, analytics, fonts, images, or third-party integrations if the policy does not allow the required sources.

A practical rollout strategy for CSP is:

  1. Inventory scripts, styles, images, fonts, APIs, and frame sources.
  2. Start with a report-only policy.
  3. Review violations.
  4. Remove unsafe inline scripts where possible.
  5. Use nonces or hashes when inline scripts are unavoidable.
  6. Move from report-only mode to enforcement.

Content Security Policy

Content Security Policy, or CSP, is one of the most powerful browser security headers. It helps reduce the risk of cross-site scripting by controlling which sources the browser may load code and resources from.

Example strict starting point:

Code
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Example policy for an application that calls an API and loads images from a CDN:

Code
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com; img-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

Common CSP mistakes include:

  • Using default-src *
  • Allowing unsafe-inline without understanding the risk
  • Allowing unsafe-eval unnecessarily
  • Forgetting frame-ancestors
  • Not testing third-party scripts and analytics
  • Using a copied policy that does not match the real application

CSP is application-specific. A strong policy for one application may break another application.

Secret Handling

A secret is any value that grants access or can be used to impersonate an identity.

Examples include:

  • Database passwords
  • API keys
  • OAuth client secrets
  • JWT signing keys
  • Encryption keys
  • Storage account keys
  • Connection strings
  • Certificates and private keys
  • Service bus connection strings

Secret handling is the practice of storing, accessing, rotating, and auditing secrets safely.

Bad secret handling example:

Code
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=prod-db;Database=AppDb;User Id=app;Password=SuperSecretPassword;"
  }
}

This is risky because configuration files are often committed, copied, logged, shared, or deployed to multiple environments.

Better options include:

  • Local development: user secrets or local environment variables
  • CI/CD: secure pipeline secret storage
  • Production: managed secret store such as Azure Key Vault
  • Azure-hosted apps: managed identity instead of stored credentials where possible

User Secrets, Environment Variables, and Key Vault

In ASP.NET Core development, User Secrets can store local development secrets outside the project folder.

Example:

Code
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=AppDb;Trusted_Connection=True;"

User Secrets are useful for local development, but they are not a production secret store.

Environment variables are often used in deployed applications:

Code
ConnectionStrings__DefaultConnection="Server=prod-db;Database=AppDb;..."

The double underscore maps to nested configuration keys in ASP.NET Core.

For production, a managed secret store is safer. In Azure, an ASP.NET Core application can load secrets from Key Vault using managed identity.

Example concept:

Code
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{builder.Configuration["KeyVaultName"]}.vault.azure.net/"),
    new DefaultAzureCredential());

With managed identity, the application does not need a client secret in its configuration. Azure provides the identity, and access to the vault is controlled through permissions.

Secret Handling Best Practices

Good secret handling habits include:

  • Never commit secrets to source control.
  • Never put production secrets in development configuration.
  • Avoid sharing secrets in chat, email, tickets, logs, or screenshots.
  • Prefer managed identities over static credentials when possible.
  • Scope each secret to the minimum required access.
  • Rotate secrets regularly and immediately after suspected exposure.
  • Use separate secrets per environment.
  • Use separate identities per application or service.
  • Mask secrets in logs and telemetry.
  • Avoid long-lived personal access tokens.
  • Audit who accessed secrets and when.
  • Delete unused secrets.

One important habit is to design the application so that secrets are read at startup or through a centralized provider, not scattered across the codebase.

Least Privilege

Least privilege means granting only the minimum permissions required to perform a task, for the minimum scope, and for the minimum necessary duration.

It applies to:

  • Human users
  • Application users
  • Service accounts
  • Managed identities
  • Database accounts
  • Cloud roles
  • CI/CD pipelines
  • API permissions
  • File and storage access

Bad example:

Code
App Service identity has Owner access to the entire Azure subscription.

Better example:

Code
App Service managed identity has Key Vault Secrets User access only on one Key Vault.
The same identity has read/write access only to the specific storage container it needs.

Least privilege limits blast radius. If a user, token, or service identity is compromised, the attacker gets fewer permissions.

Least Privilege in Application Code

Least privilege is not only an infrastructure concept. It also applies inside application code.

Examples:

  • Users should only access resources they own or are allowed to manage.
  • Admin permissions should be separated from normal user permissions.
  • Sensitive operations should require explicit authorization checks.
  • APIs should avoid returning fields the client does not need.
  • Background jobs should use separate identities from web APIs.
  • Read-only workflows should use read-only database permissions where practical.

Example policy-based authorization in ASP.NET Core:

Code
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("CanApproveOrder", policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireClaim("permission", "orders.approve");
    });
});

app.MapPost("/orders/{id:int}/approve", ApproveOrder)
   .RequireAuthorization("CanApproveOrder");

This is better than checking only whether the user is logged in.

For resource-based authorization, the application should verify access to the specific resource:

Code
if (order.CustomerId != currentUser.CustomerId && !currentUser.IsAdmin)
{
    return Results.Forbid();
}

Authentication answers: who are you?

Authorization answers: what are you allowed to do?

Resource-based authorization answers: are you allowed to do this action on this specific object?

CORS, Headers, Secrets, and Least Privilege Together

These security practices are strongest when used together.

Example production API design:

  • The API allows CORS only from https://app.example.com.
  • The API requires authentication and authorization for protected endpoints.
  • The frontend uses secure cookies or tokens based on the chosen authentication model.
  • CSRF protection is used when cookies are automatically sent.
  • Secure headers are applied consistently.
  • CSP is tuned to the actual frontend.
  • Secrets are stored in Key Vault and accessed by managed identity.
  • The managed identity has only the Key Vault and database permissions it needs.
  • Admin actions require dedicated permissions.
  • Logs never include tokens, passwords, or full connection strings.

A common interview mistake is treating one control as if it solves everything. For example, CORS does not replace authorization, CSP does not replace output encoding, Key Vault does not fix excessive permissions, and least privilege does not remove the need for input validation.

Common Mistakes

Common CORS mistakes:

  • Using AllowAnyOrigin for private APIs.
  • Allowing credentials for untrusted origins.
  • Assuming CORS protects APIs from all clients.
  • Forgetting that tools like Postman, curl, and backend services are not restricted by browser CORS.
  • Adding CORS headers manually instead of using framework policy configuration.
  • Applying CORS middleware in the wrong order.

Common secure header mistakes:

  • Adding headers without testing their behavior.
  • Using weak CSP values such as default-src *.
  • Relying only on X-Frame-Options instead of also using CSP frame-ancestors.
  • Forgetting HSTS in production HTTPS sites.
  • Caching sensitive authenticated pages.

Common secret handling mistakes:

  • Storing secrets in appsettings.json.
  • Committing .env files.
  • Logging connection strings or tokens.
  • Reusing the same secret across environments.
  • Giving developers access to production secrets by default.
  • Using long-lived credentials when managed identity is available.

Common least privilege mistakes:

  • Assigning broad roles for convenience.
  • Granting permissions at subscription or tenant scope when resource scope is enough.
  • Sharing one service account across many applications.
  • Using admin database credentials in application runtime.
  • Not reviewing stale access.
  • Ignoring authorization checks after authentication succeeds.

Best Practices Summary

For CORS:

  • Allow only known frontend origins.
  • Allow only required methods and headers.
  • Avoid credentials unless required.
  • Keep CORS configuration environment-specific.
  • Do not confuse CORS with authentication or authorization.

For secure headers:

  • Enforce HTTPS with HSTS in production.
  • Add X-Content-Type-Options: nosniff.
  • Use CSP and tune it carefully.
  • Use frame-ancestors to prevent unauthorized framing.
  • Use Referrer-Policy and Permissions-Policy.
  • Avoid caching sensitive responses.

For secrets:

  • Keep secrets out of source control.
  • Use user secrets only for local development.
  • Use managed secret stores in production.
  • Prefer managed identity over static credentials.
  • Rotate and audit secrets.
  • Mask secrets in logs.

For least privilege:

  • Grant only required permissions.
  • Use the narrowest practical scope.
  • Separate duties between users, services, and environments.
  • Review access regularly.
  • Use resource-based authorization for sensitive domain data.

Interview Practice

PreviousCookie behavior, CSRF, and browser-based security concernsNext UpJWT Bearer Auth, Claims, Scopes, and Policy-Based Authorization