DEV_NET_CORE
GET_STARTED
.NETAuthentication, authorization, and web security

Cookie behavior, CSRF, and browser-based security concerns

Overview

Cookie behavior, CSRF, and browser-based security concerns are important parts of secure web application design. They explain how browsers store authentication state, when credentials are automatically sent to a server, and how attackers can abuse that behavior if an application does not validate requests correctly.

In many web applications, especially server-rendered applications and cookie-authenticated APIs, the browser automatically includes cookies with matching requests. This is useful because users do not need to manually attach credentials to every request. However, it also creates security risks. If another website can cause the user's browser to send a state-changing request to your application, the browser may attach the user's authentication cookie even though the user did not intentionally perform the action. This is the core idea behind Cross-Site Request Forgery, usually called CSRF or XSRF.

This topic matters because authentication is not only about verifying who the user is. A secure application must also understand how browsers behave, how cookies are scoped, how cross-origin requests work, how tokens are protected, and how client-side attacks such as XSS can weaken otherwise correct authentication designs.

In interviews, this topic is commonly used to test whether a developer understands real production security rather than only framework syntax. A strong candidate should be able to explain cookie attributes such as HttpOnly, Secure, and SameSite; describe how CSRF attacks work; compare cookie-based authentication with bearer-token authentication; and design practical defenses for server-rendered apps, APIs, and SPAs.

Core Concepts

Browser Cookies

A cookie is a small piece of data stored by the browser and associated with a website. Servers usually create cookies with the Set-Cookie response header. On later matching requests, the browser sends those cookies back using the Cookie request header.

Cookies are commonly used for:

  • Authentication sessions
  • User preferences
  • Temporary state
  • Tracking and analytics
  • Anti-forgery coordination

A simplified HTTP flow looks like this:

Code
HTTP/1.1 200 OK
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax; Path=/

Later, the browser may send:

Code
GET /account HTTP/1.1
Host: example.com
Cookie: sessionId=abc123

The important security point is that cookies are automatically attached by the browser when the request matches the cookie's rules. JavaScript does not need to manually add the cookie.

In cookie-based authentication, the server signs in a user and sends an authentication cookie to the browser. The cookie may contain an encrypted authentication ticket, a reference to a server-side session, or another protected value.

In ASP.NET Core, cookie authentication usually looks conceptually like this:

Code
builder.Services
    .AddAuthentication("AppCookie")
    .AddCookie("AppCookie", options =>
    {
        options.Cookie.Name = "__Host-AppAuth";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.LoginPath = "/login";
        options.AccessDeniedPath = "/access-denied";
    });

The browser does not know whether the cookie represents an authenticated user. It only stores and sends the cookie according to browser rules. The server is responsible for validating the cookie and rebuilding the user identity.

Cookie attributes define how the browser stores and sends a cookie.

HttpOnly

HttpOnly prevents JavaScript from reading the cookie through document.cookie.

Code
Set-Cookie: sessionId=abc123; HttpOnly

This is important because if an attacker finds an XSS vulnerability, HttpOnly makes it harder to directly steal the session cookie.

However, HttpOnly does not prevent XSS itself. Malicious JavaScript can still perform actions as the user while it runs in the page.

Secure

Secure tells the browser to send the cookie only over HTTPS.

Code
Set-Cookie: sessionId=abc123; Secure

Authentication cookies should normally use Secure in production. Without it, cookies may be exposed over unencrypted HTTP.

SameSite

SameSite controls whether a cookie is sent on cross-site requests.

Common values are:

  • Strict: send the cookie only in same-site contexts.
  • Lax: send the cookie for same-site requests and some top-level navigations.
  • None: send the cookie in cross-site contexts, but it must also use Secure.

Example:

Code
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax

SameSite=Lax is a common default for normal web app session cookies because it reduces many CSRF risks while keeping normal navigation usable.

SameSite=Strict is more restrictive and can improve protection, but it may break expected flows such as links from external sites.

SameSite=None is required for some cross-site scenarios, such as embedded applications, certain third-party integrations, or some external login flows. Because it allows cross-site cookie sending, it must be paired with Secure.

Domain and Path

Domain and Path control where a cookie is sent.

Code
Set-Cookie: sessionId=abc123; Domain=example.com; Path=/admin

If no Domain is specified, the cookie is usually host-only. Host-only cookies are often safer because they are not automatically shared with subdomains.

A broad domain such as .example.com can be risky because subdomains may receive or influence cookies. If one subdomain is compromised, it may affect other applications under the same parent domain.

Expires and Max-Age

Expires and Max-Age control cookie lifetime.

Code
Set-Cookie: rememberMe=true; Max-Age=2592000

Session cookies usually expire when the browser session ends. Persistent cookies survive longer.

Authentication cookies should use a lifetime that matches the risk of the application. Banking, admin, and sensitive enterprise systems usually require shorter lifetimes and stricter reauthentication than low-risk applications.

Cookie Prefixes

Cookie prefixes help enforce stronger browser rules.

__Secure- means the cookie must be set over HTTPS and use Secure.

__Host- is stricter. A __Host- cookie must use Secure, must not specify Domain, and must use Path=/.

Example:

Code
Set-Cookie: __Host-AppAuth=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

For important authentication cookies, __Host- is often a good habit because it prevents accidental broad domain scoping.

Site, Origin, and Cross-Origin Requests

Security discussions often use the words site and origin, but they are not identical.

An origin is defined by:

  • Scheme
  • Host
  • Port

For example:

Code
https://app.example.com:443

A site is usually based on the registrable domain and scheme.

For example:

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

These may be different origins but can still be same-site depending on the domain relationship.

This distinction matters because:

  • CORS is based on origins.
  • SameSite cookie behavior is based on site concepts.
  • Browser security decisions may differ depending on whether something is cross-origin or cross-site.

Same-Origin Policy

The Same-Origin Policy is a browser security rule that restricts one origin from reading sensitive data from another origin.

For example, a malicious website may be able to cause the browser to submit a form to a banking site, but it usually cannot read the banking site's response because of the Same-Origin Policy.

This is why CSRF is often about causing state-changing actions, not reading data directly.

CORS Is Not CSRF Protection

CORS stands for Cross-Origin Resource Sharing. It controls whether browsers allow JavaScript from one origin to read responses from another origin.

CORS does not stop simple browser actions such as form posts, image requests, or top-level navigations. It also does not prove that a request was intentionally made by the real user.

A common mistake is assuming this is enough:

Code
app.UseCors();

CORS is important for API access control, but CSRF protection requires separate defenses such as anti-forgery tokens, SameSite, and origin validation.

What CSRF Is

CSRF is an attack where a malicious site causes the victim's browser to send an unwanted request to a trusted application where the victim is already authenticated.

A simplified example:

Code
<form action="https://bank.example.com/transfer" method="post">
  <input type="hidden" name="toAccount" value="attacker" />
  <input type="hidden" name="amount" value="1000" />
</form>

<script>
  document.forms[0].submit();
</script>

If the victim is already signed in to bank.example.com, the browser may automatically attach the victim's session cookie. If the server only checks that the cookie is valid, it may process the request.

CSRF usually requires these conditions:

  • The application uses automatically-sent credentials, such as cookies.
  • The victim is authenticated.
  • The target endpoint changes server-side state.
  • The endpoint does not require an attacker-unknown value, such as a valid anti-forgery token.
  • The browser can be tricked into sending the request.

Why CSRF Usually Targets State-Changing Operations

Safe HTTP methods such as GET should not change server state. State-changing actions should use methods such as POST, PUT, PATCH, or DELETE.

Bad design:

Code
app.MapGet("/delete-account", (AppDbContext db) =>
{
    // Dangerous: state-changing action via GET
});

Better design:

Code
app.MapPost("/delete-account", async (
    DeleteAccountRequest request,
    AppDbContext db,
    CancellationToken cancellationToken) =>
{
    // Validate authorization, anti-forgery protections, and business rules.
});

Using correct HTTP methods does not solve CSRF by itself, but it reduces attack surface and aligns with browser and security expectations.

Anti-Forgery Tokens

An anti-forgery token is a secret value generated by the server and required on unsafe requests. The attacker should not be able to guess or read the token from another site.

Common patterns include:

  • Synchronizer token pattern
  • Double-submit cookie pattern
  • Header-based token submission for APIs and SPAs

In ASP.NET Core MVC or Razor Pages, anti-forgery protection can be enabled globally for unsafe methods:

Code
using Microsoft.AspNetCore.Mvc;

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});

A Razor form can include an anti-forgery token:

Code
<form asp-action="UpdateEmail" method="post">
    @Html.AntiForgeryToken()

    <input name="email" />
    <button type="submit">Save</button>
</form>

The server validates that the token submitted by the form matches the expected token for that user context.

Anti-Forgery Tokens for SPAs

SPAs often call APIs using fetch or Axios. If the SPA uses cookie-based authentication, CSRF is still relevant because the browser can automatically attach the authentication cookie.

A common approach is:

  1. Server issues an anti-forgery token.
  2. Browser stores the token in a readable place, such as a non-HttpOnly CSRF token cookie.
  3. JavaScript reads that token.
  4. JavaScript sends the token in a custom request header.
  5. Server validates the token.

Example client request:

Code
await fetch("/api/profile/email", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "X-CSRF-TOKEN": csrfToken
  },
  body: JSON.stringify({ email: "[email protected]" })
});

Example ASP.NET Core setup:

Code
builder.Services.AddAntiforgery(options =>
{
    options.HeaderName = "X-CSRF-TOKEN";
});

A custom header helps because normal cross-site HTML forms cannot add arbitrary headers. However, the server must still validate the token, and CORS must not allow untrusted origins to send credentialed requests.

SameSite as CSRF Defense-in-Depth

SameSite reduces how often cookies are sent in cross-site contexts.

For many normal web applications:

Code
options.Cookie.SameSite = SameSiteMode.Lax;

For stricter applications:

Code
options.Cookie.SameSite = SameSiteMode.Strict;

For cross-site authentication or embedded app scenarios:

Code
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;

SameSite is useful, but it should not always be the only CSRF defense. Complex systems, legacy browsers, subdomain risks, external identity providers, and unusual navigation flows can make token-based protection necessary.

Origin and Referer Validation

For unsafe requests, the server can check Origin or Referer headers to ensure the request came from an expected origin.

Example idea:

Code
app.Use(async (context, next) =>
{
    if (HttpMethods.IsPost(context.Request.Method) ||
        HttpMethods.IsPut(context.Request.Method) ||
        HttpMethods.IsPatch(context.Request.Method) ||
        HttpMethods.IsDelete(context.Request.Method))
    {
        var origin = context.Request.Headers.Origin.ToString();

        if (!string.IsNullOrEmpty(origin) &&
            origin != "https://app.example.com")
        {
            context.Response.StatusCode = StatusCodes.Status403Forbidden;
            return;
        }
    }

    await next();
});

Origin validation is usually defense-in-depth, not a complete replacement for anti-forgery tokens. Some requests may omit these headers, and applications must decide how strictly to handle missing values.

Fetch Metadata Headers

Modern browsers may send Fetch Metadata headers such as:

Code
Sec-Fetch-Site
Sec-Fetch-Mode
Sec-Fetch-Dest

These headers help servers detect cross-site requests. For example, a server can reject cross-site unsafe requests when Sec-Fetch-Site indicates the request is from another site.

This is useful defense-in-depth, especially for applications that want to block unexpected cross-site traffic. It should be tested carefully because integrations, embeds, identity providers, and legitimate cross-site flows may be affected.

Cookie-based authentication and bearer-token authentication have different browser security trade-offs.

Cookie-based authentication:

  • Browser automatically sends cookies.
  • Works well for server-rendered apps.
  • Can use HttpOnly to reduce token theft through XSS.
  • Requires CSRF protection for state-changing operations.
  • Needs careful SameSite, Secure, and domain configuration.

Bearer-token authentication:

  • Client usually sends the token manually in the Authorization header.
  • Common for APIs and mobile clients.
  • Classic CSRF risk is lower if the token is not automatically sent by the browser.
  • XSS risk can be higher if tokens are stored in localStorage or accessible JavaScript memory.
  • Token refresh and logout can be more complex.

Example bearer request:

Code
GET /api/orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOi...

A good interview answer should avoid saying one approach is always better. The right choice depends on the client type, threat model, deployment model, authentication provider, and operational requirements.

XSS and Its Relationship to Cookies

XSS means an attacker can run JavaScript in the victim's browser within the trusted application's origin.

HttpOnly helps prevent JavaScript from reading cookies:

Code
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Lax

But XSS can still be dangerous because malicious JavaScript can:

  • Send requests as the user
  • Read non-HttpOnly tokens
  • Modify page content
  • Capture user input
  • Trigger sensitive workflows
  • Exfiltrate data visible in the page

This is why cookie security and XSS prevention must work together.

Common XSS defenses include:

  • Output encoding
  • Avoiding unsafe HTML injection
  • Sanitizing user-generated HTML when HTML input is truly required
  • Content Security Policy
  • Avoiding inline script when possible
  • Secure framework defaults
  • Dependency hygiene
  • Validating and constraining input

CSRF Tokens and XSS

CSRF tokens protect against attackers on other sites. They do not protect well against an attacker who can run JavaScript inside your own site.

If an application has XSS, malicious JavaScript may be able to read page tokens, call APIs, or perform actions directly.

This is why security is layered:

  • CSRF protection handles cross-site request abuse.
  • XSS prevention protects the trusted origin itself.
  • Authorization checks ensure the user is allowed to perform the action.
  • Audit logs and anomaly detection help detect abuse.
  • Step-up authentication protects sensitive actions.

Credentialed CORS Requests

A cross-origin API call that includes cookies requires careful server and client configuration.

Client example:

Code
await fetch("https://api.example.com/orders", {
  method: "GET",
  credentials: "include"
});

Server configuration must explicitly allow the trusted origin and credentials:

Code
builder.Services.AddCors(options =>
{
    options.AddPolicy("SpaClient", policy =>
    {
        policy
            .WithOrigins("https://app.example.com")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

Avoid this pattern with credentials:

Code
policy
    .AllowAnyOrigin()
    .AllowCredentials();

Credentialed CORS should be restricted to specific trusted origins. Allowing broad origins with credentials can expose sensitive APIs.

Browser Storage Choices

Browser applications commonly store state in:

  • Cookies
  • localStorage
  • sessionStorage
  • In-memory variables
  • IndexedDB

Security trade-offs:

Storage optionAutomatically sent?Readable by JavaScript?CSRF riskXSS token theft risk
HttpOnly cookieYesNoHigher without CSRF defenseLower direct theft risk
Non-HttpOnly cookieYesYesHigher without CSRF defenseHigher
localStorageNoYesLower classic CSRF riskHigher
In-memory tokenNoYes while app runsLower classic CSRF riskStill exposed during XSS

For browser apps, the decision is not simply "cookies are secure" or "tokens are secure." Each option changes the attack surface.

Session Expiration and Logout

Cookie security also includes lifecycle management.

Important practices include:

  • Use reasonable expiration times.
  • Rotate session identifiers after login.
  • Invalidate server-side sessions on logout when using server-side session storage.
  • Clear cookies on logout.
  • Consider sliding expiration carefully.
  • Require reauthentication for sensitive operations.
  • Avoid long-lived authentication cookies for high-risk apps.

Example logout cookie clearing:

Code
await HttpContext.SignOutAsync("AppCookie");

If the authentication state is fully contained in a protected cookie, logout usually removes the browser cookie. If the system uses server-side sessions or refresh tokens, the server should also revoke or invalidate the related server-side state.

Clickjacking

Clickjacking tricks users into clicking hidden or disguised UI elements, often by embedding the target site in an iframe.

Defenses include:

  • Content-Security-Policy: frame-ancestors 'self'
  • X-Frame-Options: DENY
  • X-Frame-Options: SAMEORIGIN

Example:

Code
Content-Security-Policy: frame-ancestors 'self'

This matters for authentication and CSRF because attackers may combine UI deception with authenticated user actions.

Content Security Policy

Content Security Policy, or CSP, helps reduce the impact of XSS by controlling where scripts, styles, images, frames, and other resources can be loaded from.

Example:

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

CSP is not a replacement for output encoding or secure coding, but it is valuable defense-in-depth.

HTTPS and HSTS

HTTPS protects cookies and credentials in transit. HSTS tells browsers to use HTTPS for future requests to the site.

Example:

Code
Strict-Transport-Security: max-age=31536000; includeSubDomains

For production authentication systems, HTTPS should be mandatory. Authentication cookies should use Secure, and HTTP endpoints should redirect or be disabled.

A practical cookie configuration might look like this:

Code
builder.Services
    .AddAuthentication("AppCookie")
    .AddCookie("AppCookie", options =>
    {
        options.Cookie.Name = "__Host-AppAuth";
        options.Cookie.HttpOnly = true;
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
        options.Cookie.SameSite = SameSiteMode.Lax;
        options.Cookie.Path = "/";

        options.ExpireTimeSpan = TimeSpan.FromHours(1);
        options.SlidingExpiration = true;

        options.LoginPath = "/login";
        options.LogoutPath = "/logout";
        options.AccessDeniedPath = "/access-denied";
    });

Important notes:

  • Use HTTPS in production.
  • Use HttpOnly for authentication cookies.
  • Use Secure for authentication cookies.
  • Prefer host-only cookies for authentication.
  • Use SameSite=Lax or Strict when possible.
  • Use SameSite=None; Secure only when cross-site cookie sending is required.
  • Add anti-forgery validation for unsafe methods when cookies authenticate browser requests.

Middleware Ordering and Security

Security features often depend on correct ASP.NET Core middleware order.

A common order is:

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

app.UseHttpsRedirection();

app.UseStaticFiles();

app.UseRouting();

app.UseCors("SpaClient");

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

General guidance:

  • HTTPS redirection and HSTS should happen early.
  • Routing should happen before authentication and authorization.
  • CORS should be placed where it can apply to endpoints correctly.
  • Authentication must run before authorization.
  • Endpoint mappings should happen after required middleware setup.

Security mistakes can happen even when individual features are configured correctly but placed in the wrong order.

Common Mistakes

Common mistakes include:

  • Assuming CORS prevents CSRF.
  • Using GET for state-changing operations.
  • Storing raw sensitive data in cookies.
  • Forgetting HttpOnly on authentication cookies.
  • Forgetting Secure in production.
  • Using SameSite=None without understanding cross-site risk.
  • Using broad cookie domains unnecessarily.
  • Trusting authentication without checking authorization.
  • Storing long-lived access tokens in localStorage without considering XSS.
  • Disabling anti-forgery validation because it is inconvenient during development.
  • Allowing credentialed CORS from untrusted origins.
  • Believing HttpOnly prevents XSS.
  • Believing CSRF tokens protect against XSS.
  • Not testing login flows, external identity providers, iframes, and SPAs with real browser behavior.

Best Practices

Good security habits include:

  • Use HTTPS everywhere.
  • Mark authentication cookies as HttpOnly.
  • Mark authentication cookies as Secure.
  • Use SameSite=Lax or Strict when possible.
  • Use SameSite=None; Secure only when cross-site cookies are required.
  • Prefer host-only cookies or __Host- prefixed cookies for authentication.
  • Protect unsafe methods with anti-forgery tokens when using cookie authentication.
  • Keep GET operations safe and idempotent.
  • Validate authorization on every sensitive operation.
  • Restrict credentialed CORS to trusted origins only.
  • Add origin or Fetch Metadata checks for defense-in-depth when appropriate.
  • Use CSP, output encoding, and safe rendering to reduce XSS risk.
  • Avoid storing sensitive raw data in client-side storage.
  • Use short-lived sessions for sensitive applications.
  • Require step-up authentication for high-risk actions.
  • Test browser security behavior in environments that match production.

Interview Practice

PreviousAuthentication vs Authorization in C#Next UpCORS, secure headers, secret handling, and least privilege