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:
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:
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, orDELETE - Custom request headers such as
AuthorizationorX-Correlation-Id - Certain content types, such as
application/json
Example preflight request:
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:
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:
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:
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
SameSitecookies- Origin or Referer validation for sensitive operations
- Safe HTTP semantics, where
GETdoes 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:
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-Securitytells browsers to use HTTPS for future requests.Content-Security-Policyrestricts where scripts, styles, images, frames, and other resources can be loaded from.X-Content-Type-Options: nosniffprevents MIME-sniffing attacks.X-Frame-Optionshelps prevent clickjacking in older browser scenarios.frame-ancestorsin CSP is the modern way to control who can embed the page.Referrer-Policycontrols how much referrer information is sent to other sites.Permissions-Policylimits browser features such as camera, microphone, geolocation, and payment APIs.Cache-Controlcan 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:
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:
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:
- Inventory scripts, styles, images, fonts, APIs, and frame sources.
- Start with a report-only policy.
- Review violations.
- Remove unsafe inline scripts where possible.
- Use nonces or hashes when inline scripts are unavoidable.
- 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:
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:
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-inlinewithout understanding the risk - Allowing
unsafe-evalunnecessarily - 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:
{
"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:
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:
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:
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:
App Service identity has Owner access to the entire Azure subscription.
Better example:
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:
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:
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
AllowAnyOriginfor 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-Optionsinstead of also using CSPframe-ancestors. - Forgetting HSTS in production HTTPS sites.
- Caching sensitive authenticated pages.
Common secret handling mistakes:
- Storing secrets in
appsettings.json. - Committing
.envfiles. - 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-ancestorsto prevent unauthorized framing. - Use
Referrer-PolicyandPermissions-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.