Skip to content

CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute - PHP

Overview

The Secure attribute on an HTTP cookie instructs the browser to only transmit that cookie over encrypted HTTPS connections. Without this attribute, the browser also sends the cookie on plaintext HTTP requests. If a user is directed to an HTTP version of a page - by following an HTTP link, by typing the bare hostname into the address bar, or because an on-path attacker injected a redirect to http:// - the session cookie travels in the clear and can be intercepted by any observer on the network path.

In PHP, setcookie() does not set the Secure flag by default. The same applies to PHP session cookies: session_start() uses whatever settings are in php.ini, and session.cookie_secure defaults to 0.

Primary Defence: Pass 'secure' => true in the options array to every setcookie() call. Configure session.cookie_secure = 1 in php.ini (or via session_set_cookie_params() before session_start()). Add 'httponly' => true for any cookie no page script needs to read. SameSite 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

setcookie() Without Secure Flag

<?php
// VULNERABLE - cookie sent over HTTP as well as HTTPS
setcookie('auth_token', $tokenValue, time() + 3600, '/');

// Legacy positional form - secure is the sixth argument, and nothing names it
setcookie('session_pref', $preference, time() + 86400, '/', '', false, true);
//                                                             ^--- secure (FALSE!)

Why this is vulnerable:

  • Omitting the secure parameter (which defaults to false) means the browser will include this cookie in HTTP requests. An attacker on the same network can capture the auth_token cookie and impersonate the user.
<?php
// VULNERABLE - session cookie not configured as Secure
session_start(); // Uses php.ini defaults; session.cookie_secure defaults to 0

// Later the session ID cookie is sent on both HTTP and HTTPS requests
$_SESSION['user_id'] = $userId;

Why this is vulnerable:

  • The PHP default session.cookie_secure = 0 sends the PHPSESSID cookie over HTTP. A compromised network path exposes the session identifier, allowing session hijacking.

setcookie() Using Positional Parameters

<?php
// VULNERABLE - positional form makes it easy to accidentally set secure=false
setcookie(
    'remember_me',  // name
    $token,         // value
    time() + 2592000, // expires
    '/',            // path
    '',             // domain
    false,          // VULNERABLE - secure = false
    true            // httponly = true
);

Why this is vulnerable:

  • The positional (pre-PHP 7.3) syntax places secure at parameter position 6. Accidentally passing false or omitting it means the cookie is sent over plaintext HTTP as well as HTTPS.

Secure Patterns

setcookie() with Options Array (PHP 7.3+)

<?php
// SECURE - options array makes each attribute explicit and readable
setcookie('auth_token', $tokenValue, [
    'expires'  => time() + 3600,
    'path'     => '/',
    'domain'   => '',        // current host only
    'secure'   => true,      // HTTPS only
    'httponly' => true,      // not accessible via JavaScript
    'samesite' => 'Strict',  // not sent on cross-site requests (CSRF protection)
]);

Why this works:

  • The options array form (PHP 7.3+) makes every cookie attribute explicitly named, eliminating positional mistakes. Setting 'secure' => true ensures the browser only sends the cookie over HTTPS.
<?php
// SECURE - configure session cookie before session_start()
session_set_cookie_params([
    'lifetime' => 0,          // session cookie (deleted on browser close)
    'path'     => '/',
    'domain'   => '',
    'secure'   => true,       // HTTPS only
    'httponly' => true,
    'samesite' => 'Strict',
]);
session_start();

// Regenerate session ID after authentication to prevent fixation
session_regenerate_id(true);
$_SESSION['user_id'] = $authenticatedUserId;

Why this works:

  • Calling session_set_cookie_params() before session_start() configures the PHPSESSID cookie with the Secure flag. session_regenerate_id(true) prevents session fixation attacks by replacing the session ID after login.
; php.ini - enforce secure cookie settings at the server level
session.cookie_secure   = 1    ; Session cookies HTTPS only
session.cookie_httponly = 1    ; Session cookies not accessible via JS
session.cookie_samesite = Strict  ; Prevent CSRF via cross-site requests

; Optional: make the browser enforce the flag as well as PHP setting it
session.name = __Secure-PHPSESSID  ; only accepted if Secure is actually present

Why this works:

  • php.ini settings apply to every PHP script on the server, so the flag holds even for code that never calls session_set_cookie_params(). The __Secure- cookie name prefix instructs the browser to reject the cookie if it arrives without the Secure attribute, so a deployment that quietly loses session.cookie_secure fails loudly instead of silently issuing a plaintext session cookie.

Change the name only once you have confirmed session.cookie_secure is actually in force on the pool serving requests. PHP accepts the prefixed name regardless: with the flag off it will happily emit Set-Cookie: __Secure-PHPSESSID=...; path=/ with no Secure, and every browser then discards it. The application does not error - it simply never has a session, which reads as a login loop rather than as a cookie problem. That is the same trade the __Host- prefix makes: it converts a silent weakness into a visible outage, which is what you want, but only after the flag is verified on the wire.

Considerations

Strict is a choice, not the safe default. The examples on this page use Strict because they issue cookies for flows that begin on the application's own pages. Strict withholds the cookie on every cross-site request, including a user arriving from an email link, a search result, a partner portal or an identity provider's redirect - they land signed out, then appear signed in after any same-site click. It reads as a session bug and it is a configuration choice. Lax still withholds the cookie from cross-site POSTs and subresource loads, which is the CSRF-relevant part, while sending it on top-level navigations. Use Strict only where nothing legitimate navigates in from elsewhere, and treat OAuth and SSO callbacks as requiring Lax outright, because the provider's redirect is a cross-site navigation. None of this changes Secure, which is what this finding is about and which is required either way.

session.cookie_samesite = Strict in php.ini applies that choice to every site served by the pool, so a single OAuth-based application among them is enough to make it the wrong setting there. Set session.cookie_secure = 1 globally - it has no per-application exception - and leave samesite to session_set_cookie_params() in the applications that need something other than the default.

Testing

  • Normal input: sign in over HTTPS and confirm session, authentication, and preference cookies are set and accepted.
  • Boundary input: test local development, staging behind a proxy, and redirects from HTTP to HTTPS to ensure cookie behavior is intentional.
  • Malicious input: make an HTTP request to the same host and verify sensitive cookies are absent from the request headers.

Common Pitfalls

  • Setting session.cookie_secure = 1 in one php.ini while the request is actually served by a different SAPI or pool configuration (CLI vs. PHP-FPM pool, or a .user.ini/php_value override at the vhost level) that doesn't carry the same setting - PHP configuration is layered and per-SAPI, so a value confirmed in one context doesn't guarantee it applies to the process handling real requests.
  • Calling session_set_cookie_params() with 'secure' => true after session_start() has already run - the parameters only take effect for the next session start, so a session already begun under the previous (insecure) defaults keeps its original cookie attributes until it's regenerated.
  • Using the legacy positional setcookie() signature and counting arguments to reach the secure parameter - the pre-7.3 positional form accepts a shifted argument list without any type or count error, so an unrelated refactor elsewhere in the call can silently move secure into the wrong position.

Additional Resources