Skip to content

CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute - C# / ASP.NET Core

Overview

The Secure attribute on a cookie instructs the browser to only transmit the cookie over encrypted HTTPS connections. Without it, the cookie is also sent over plaintext HTTP. If a user navigates to an HTTP version of a page (or is redirected to one by an attacker), the browser sends the session cookie in the clear, allowing a network-level attacker to steal it.

In ASP.NET Core the default differs per cookie source, and the difference decides how bad the finding is. Response.Cookies.Append() uses CookieOptions.Secure = false. AddSession() defaults its cookie to CookieSecurePolicy.None, which never emits the attribute - not even for a session created over HTTPS. Cookie authentication and any bare CookieBuilder default to CookieSecurePolicy.SameAsRequest, which emits it when the request the app saw was HTTPS and silently drops it otherwise. The fix requires setting the flag at both the individual cookie level and globally via CookiePolicyOptions.

Primary Defence: Set Secure = true on every CookieOptions and configure CookiePolicyOptions.Secure = CookieSecurePolicy.Always as a global default - Secure is the fix for this finding and is safe to force globally. Enable HTTPS redirection (UseHttpsRedirection()) and HSTS (UseHsts()) so no session exists over HTTP. Do not force SameSite the same way - it is chosen per flow, not set to Strict by default: use Strict only where nothing legitimate navigates in from another site, and Lax for OAuth/SSO callbacks and ordinary inbound links.

Common Vulnerable Patterns

// VULNERABLE - cookie transmitted over HTTP as well as HTTPS
Response.Cookies.Append("auth_token", tokenValue, new CookieOptions
{
    HttpOnly = true,
    SameSite = SameSiteMode.Strict,
    // Secure is false by default - cookie will be sent over HTTP
});

Why this is vulnerable:

  • Without Secure = true, the browser sends the auth_token cookie on HTTP requests too. An attacker on the same network (corporate Wi-Fi, ISP, or a man-in-the-middle) can capture it and impersonate the victim's session.

Session Configuration Without Secure Policy

// VULNERABLE - session cookie not flagged as Secure
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    // options.Cookie.SecurePolicy is not set - defaults to CookieSecurePolicy.None,
    // which never emits Secure, even on a session created over HTTPS
});

Why this is vulnerable:

  • SessionOptions is the one place ASP.NET Core does not fall back to SameAsRequest: its cookie builder sets SecurePolicy = CookieSecurePolicy.None explicitly, so the session cookie ships without Secure on every request, HTTPS included. Nothing about the deployment changes that - an all-HTTPS production site with a valid certificate still hands the browser a session cookie it will replay over plain HTTP. This is the default most often mistaken for SameAsRequest, and the mistake makes the finding look conditional when it is not.
// VULNERABLE - authentication cookie may be sent over HTTP
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Strict;
        // SecurePolicy not set - defaults to SameAsRequest, so Secure is derived
        // from the scheme this process saw, not the one the browser used
    });

Why this is vulnerable:

  • Cookie authentication defaults to CookieSecurePolicy.SameAsRequest, so on a request that arrived at the app over HTTPS the flag is set and a local test looks clean. The flag disappears on exactly the requests you are unlikely to test: a sign-in reached over plain HTTP before a redirect takes effect, and any deployment where a load balancer or ingress terminates TLS and forwards plain HTTP while X-Forwarded-Proto is neither forwarded nor trusted. The authentication cookie is a session credential, so a single request that omits the flag is enough for the browser to replay it in the clear afterwards.

Secure Patterns

// Program.cs - enforce Secure flag globally on ALL cookies
builder.Services.Configure<CookiePolicyOptions>(options =>
{
    // Secure is the one attribute safe to force everywhere
    options.Secure = CookieSecurePolicy.Always;

    // HttpOnly and SameSite are per-cookie decisions, and this middleware
    // overrides them rather than supplying a default - leave both alone
    options.HttpOnly = HttpOnlyPolicy.None;
    options.MinimumSameSitePolicy = SameSiteMode.Unspecified;
});

var app = builder.Build();

app.UseHttpsRedirection(); // Redirect HTTP -> HTTPS
app.UseHsts();             // Instruct browsers to always use HTTPS (after first visit)
app.UseCookiePolicy();     // Apply the policy above

Why this works:

  • CookieSecurePolicy.Always overrides the Secure flag on every cookie, including those set by third-party middleware that may not set it themselves. UseHsts() instructs browsers to refuse HTTP connections after the first HTTPS visit.

Secure is the only one of the three safe to force globally, which is why it is the only one set here. It has no legitimate exception on an HTTPS site, so raising it everywhere can only help, and forcing it is what closes this finding.

The other two are per-cookie decisions, and this middleware overrides them rather than supplying a default for cookies that did not choose. Despite its name MinimumSameSitePolicy is not a floor: set to Strict it rewrites every cookie to Strict, including one whose own CookieOptions explicitly asked for None. ASP.NET Core's OAuth and OpenID Connect handlers default CorrelationCookie to SameSite=None with SecurePolicy = Always precisely because the provider returns the user through a cross-site POST or navigation, so forcing Strict means the browser withholds that cookie at the callback and every external sign-in fails with Correlation failed - an error naming nothing about cookie policy.

HttpOnlyPolicy.Always behaves the same way. It overrides new CookieOptions { HttpOnly = false }, and the cookie most likely to have set that deliberately is the antiforgery token in a SPA: the front end reads XSRF-TOKEN from document.cookie and echoes it in an X-XSRF-TOKEN header, which is the pattern Angular and most SPA clients expect. Force HttpOnly and the script can no longer read it, so every state-changing request fails antiforgery validation with a 400 - and the cookie still looks correct in the response. Set HttpOnly on the cookies you own, where you know whether a script needs the value.

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // SECURE
        options.Cookie.HttpOnly     = true;
        options.Cookie.SameSite     = SameSiteMode.Strict;
        options.Cookie.Name         = "__Host-Auth"; // __Host- prefix forces Secure + Path=/
        options.SlidingExpiration   = true;
        options.ExpireTimeSpan      = TimeSpan.FromHours(8);
    });

Why this works:

  • CookieSecurePolicy.Always ensures the Secure attribute is always set. The __Host- cookie name prefix adds a browser-enforced check: the browser will reject the cookie if it arrives without Secure, with a Domain attribute, or with a Path other than /.

SameSiteMode.Strict here is a choice about this application, not a general recommendation. It suits an app whose users always start from its own pages. If sign-in goes through an external identity provider, or users routinely arrive from email links or a portal, Strict means the browser withholds the authentication cookie on that first cross-site navigation and the user lands signed out - then appears signed in after any same-site click. Use SameSiteMode.Lax there: it still withholds the cookie from cross-site POSTs and subresource loads, which is the CSRF-relevant part.

builder.Services.AddSession(options =>
{
    options.IdleTimeout              = TimeSpan.FromMinutes(30);
    options.Cookie.SecurePolicy      = CookieSecurePolicy.Always; // SECURE
    options.Cookie.HttpOnly          = true;
    options.Cookie.SameSite          = SameSiteMode.Strict;
    options.Cookie.IsEssential       = true;
});

Why this works: CookieSecurePolicy.Always sets the Secure attribute unconditionally. Setting it is not optional hardening here: the session cookie's default is CookieSecurePolicy.None, so without this line the attribute is never emitted at all, and no amount of correct TLS configuration will produce it. Always also avoids the trap in the alternative - SameAsRequest derives the flag from the scheme the app process saw, which behind a TLS-terminating proxy is plain HTTP, and the request that reveals that is not the one you test locally.

Three of these settings are overrides, not restatements of the defaults. ASP.NET Core defaults the session cookie to SameSite=Lax and HttpOnly=true, so SameSiteMode.Strict is a deliberate tightening: Lax still sends the cookie on top-level GET navigations, which is enough for some CSRF variants, while Strict withholds it on every cross-site request.

IsEssential = true is not a security setting and is easy to misread as one. Session cookies are not marked essential by default, which means that under the GDPR consent model the session silently fails to function until the visitor accepts tracking. Setting it declares the cookie necessary for operation so it is exempt from consent - include it so the secure configuration actually takes effect, not because it hardens anything.

// SECURE - explicit options on a manually set cookie
Response.Cookies.Append("preference", userPreference, new CookieOptions
{
    Secure   = true,
    HttpOnly = true,
    SameSite = SameSiteMode.Strict,
    Expires  = DateTimeOffset.UtcNow.AddDays(30),
    Path     = "/",
});

Why this works:

  • Explicitly setting Secure = true prevents the flag from being omitted regardless of global policy. Using SameSite = Strict also prevents the cookie from being sent on cross-site requests, mitigating CSRF.

Testing

  • Normal input: sign in over HTTPS and confirm authentication, session, and preference cookies continue to work.
  • Boundary input: test staging and reverse-proxy deployments where X-Forwarded-Proto or HTTPS termination affects request scheme detection.
  • Malicious input: browse the HTTP origin directly and inspect requests; sensitive cookies must not be sent without HTTPS.

Common Pitfalls

  • Setting CookiePolicyOptions.Secure = CookieSecurePolicy.Always but never calling app.UseCookiePolicy(), or registering it after the middleware/endpoints that already write cookies - the policy only rewrites cookies that pass through it, so an unregistered or misordered middleware leaves earlier cookies exactly as their CookieOptions specified.
  • Relying on CookieSecurePolicy.SameAsRequest as if it "does the right thing automatically" - it inspects HttpContext.Request.IsHttps at the moment the cookie is written, so any request that reaches the app over plain HTTP (direct access, or a reverse proxy that isn't forwarding/trusted for X-Forwarded-Proto) issues a non-Secure cookie even though the public-facing site is TLS-terminated elsewhere.
  • Using the __Host- cookie name prefix without also explicitly setting Secure = true/SecurePolicy = Always - the prefix makes browsers reject the cookie if it arrives without Secure, but ASP.NET Core does not infer Secure from the name; omitting the flag produces a cookie the browser silently drops, which looks like a fix in code review but breaks the cookie instead of securing it.

Additional Resources