Skip to content

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

Overview

Sensitive cookies without the 'Secure' attribute in Java web applications occur when cookies containing sensitive data (session IDs, authentication tokens, CSRF tokens) are created without the secure attribute set to true. The browser then sends the cookie over unencrypted HTTP as well as HTTPS, so anyone on the network path can read it and replay the session.

Common Java Vulnerability Scenarios:

  • Servlet cookies without setSecure(true)
  • Spring Boot cookies missing secure attribute
  • JAX-RS cookies without security flags
  • JSP session cookies transmitted over HTTP
  • Jakarta EE cookies with improper configuration
  • OAuth tokens in insecure cookies

Java Framework Cookie Security:

  • Servlet API: cookie.setSecure(true); cookie.setHttpOnly(true);
  • Spring Boot: server.servlet.session.cookie.secure=true
  • JAX-RS: NewCookie.Builder.newInstance().secure(true).httpOnly(true)
  • Jakarta EE: Cookie attributes in web.xml or programmatically

Primary Defence: Set secure=true on every cookie containing sensitive data, and enforce HTTPS in production. secure is the fix for this finding and has no legitimate exception on an HTTPS site. httpOnly=true belongs on any cookie no page script needs to read, which is nearly all of them. 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

// VULNERABLE - Session cookie without secure flag
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
import java.util.UUID;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticateUser(username, password)) {
            String sessionToken = UUID.randomUUID().toString();

            // VULNERABLE - Cookie missing secure flag
            Cookie sessionCookie = new Cookie("JSESSIONID", sessionToken);
            sessionCookie.setMaxAge(3600);  // 1 hour
            sessionCookie.setHttpOnly(true);  // Good, but not enough
            sessionCookie.setPath("/");
            // Missing: sessionCookie.setSecure(true);

            response.addCookie(sessionCookie);
            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private boolean authenticateUser(String username, String password) {
        // Authentication logic
        return true;
    }
}

Why this is vulnerable:

  • Cookie transmitted over HTTP
  • Anyone on the network path - an on-path attacker, or anything sniffing a shared network - can read the session token
  • A captured token is enough to hijack the session

Spring Boot Without Secure Cookies

SecurityConfig.java
// VULNERABLE - Spring Boot application without secure cookie configuration

// application.properties - VULNERABLE
// server.servlet.session.cookie.secure=false  # BAD!
// server.servlet.session.cookie.http-only=true
// server.servlet.session.cookie.same-site=strict

package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class VulnerableSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            );

        // VULNERABLE - Not configuring cookie security
        // Missing secure cookie configuration

        return http.build();
    }
}
LoginController.java
package com.example.controller;

import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;

@RestController
@RequestMapping("/api")
public class LoginController {

    @PostMapping("/login")
    public LoginResponse login(
            @RequestParam String username,
            @RequestParam String password,
            HttpServletResponse response) {

        if (authenticateUser(username, password)) {
            String token = UUID.randomUUID().toString();

            // VULNERABLE - Custom cookie without secure flag
            Cookie authCookie = new Cookie("auth_token", token);
            authCookie.setMaxAge(3600);
            authCookie.setHttpOnly(true);
            authCookie.setPath("/");
            // Missing: authCookie.setSecure(true);

            response.addCookie(authCookie);

            return new LoginResponse("logged_in", username);
        }

        throw new RuntimeException("Invalid credentials");
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }

    static class LoginResponse {
        public String status;
        public String username;

        public LoginResponse(String status, String username) {
            this.status = status;
            this.username = username;
        }
    }
}

Why this is vulnerable:

  • Application properties don't set secure=true, so the container's session cookie has no Secure attribute
  • The custom auth_token cookie never calls setSecure(true) either
  • Both are sent over HTTP wherever the application answers on HTTP
// VULNERABLE - JAX-RS REST API with insecure cookies
package com.example.api;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.util.UUID;

@Path("/auth")
public class VulnerableAuthResource {

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(
            @FormParam("username") String username,
            @FormParam("password") String password) {

        if (authenticateUser(username, password)) {
            String sessionToken = UUID.randomUUID().toString();

            // VULNERABLE - NewCookie without secure flag
            NewCookie cookie = new NewCookie(
                "session_id",           // name
                sessionToken,           // value
                "/",                    // path
                null,                   // domain
                null,                   // comment
                3600,                   // maxAge
                false                   // secure - SHOULD BE TRUE!
            );

            return Response.ok()
                .entity("{\"status\": \"logged_in\"}")
                .cookie(cookie)
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("{\"error\": \"Invalid credentials\"}")
            .build();
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }
}

Why this is vulnerable:

  • NewCookie secure parameter set to false
  • Cookie sent over HTTP, where the session token can be intercepted
  • Whoever holds that token is authenticated to the REST API
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- VULNERABLE configuration -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <session-config>
        <session-timeout>30</session-timeout>
        <cookie-config>
            <http-only>true</http-only>
            <!-- VULNERABLE - secure not set to true -->
            <!-- <secure>true</secure> -->
        </cookie-config>
    </session-config>

</web-app>
LoginServlet.java
package com.example.servlet;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/session-login")
public class SessionLoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticateUser(username, password)) {
            // VULNERABLE - Session cookie inherits web.xml config (no secure flag)
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            session.setAttribute("authenticated", true);
            session.setMaxInactiveInterval(1800);  // 30 minutes

            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }
}

Why this is vulnerable:

  • web.xml missing <secure>true</secure>
  • The container-managed session cookie is transmitted over HTTP
  • The fix belongs in the deployment descriptor, not in the servlet code
// VULNERABLE - Remember-me functionality with insecure cookie
package com.example.auth;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;

@WebServlet("/remember-login")
public class RememberMeLoginServlet extends HttpServlet {

    private static final SecureRandom secureRandom = new SecureRandom();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");
        boolean rememberMe = "true".equals(request.getParameter("remember_me"));

        if (authenticateUser(username, password)) {
            String sessionToken = generateSecureToken();

            // Session cookie (also vulnerable but short-lived)
            Cookie sessionCookie = new Cookie("session_id", sessionToken);
            sessionCookie.setMaxAge(3600);  // 1 hour
            sessionCookie.setHttpOnly(true);
            sessionCookie.setPath("/");
            // Missing: sessionCookie.setSecure(true);

            response.addCookie(sessionCookie);

            if (rememberMe) {
                String rememberToken = generateSecureToken();

                // VULNERABLE - Long-lived remember-me cookie without secure flag
                Cookie rememberCookie = new Cookie("remember_me", rememberToken);
                rememberCookie.setMaxAge(30 * 24 * 3600);  // 30 days - VERY vulnerable!
                rememberCookie.setHttpOnly(true);
                rememberCookie.setPath("/");
                // Missing: rememberCookie.setSecure(true);

                response.addCookie(rememberCookie);

                // Store token in database
                storeRememberToken(username, rememberToken);
            }

            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private String generateSecureToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }

    private void storeRememberToken(String username, String token) {
        // Database storage
    }
}

Why this is vulnerable:

  • The remember-me cookie has no secure flag and a 30-day lifetime
  • Every plain-HTTP request in those 30 days is another chance to capture it
  • A captured remember-me token still logs an attacker in long after the session cookie has expired
// VULNERABLE - Jakarta EE (Java EE) application with insecure cookies
package com.example.jakartaee;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
import java.util.UUID;

@WebServlet("/jakarta-login")
public class JakartaLoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticateUser(username, password)) {
            String token = UUID.randomUUID().toString();

            // VULNERABLE - Jakarta Cookie without secure flag
            Cookie authCookie = new Cookie("auth_token", token);
            authCookie.setMaxAge(3600);
            authCookie.setHttpOnly(true);
            authCookie.setPath("/");
            // Missing: authCookie.setSecure(true);

            response.addCookie(authCookie);
            response.setContentType("application/json");
            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }
}

Why this is vulnerable:

  • The cookie sets HttpOnly, Path and MaxAge but not setSecure(true)
  • Jakarta EE defaults the flag to off, so it has to be set explicitly on every cookie
  • The auth token is sent over HTTP and can be replayed
// VULNERABLE - Spring Session with custom cookie configuration
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class VulnerableSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSIONID");
        serializer.setCookiePath("/");
        serializer.setCookieMaxAge(1800);  // 30 minutes
        serializer.setUseHttpOnlyCookie(true);
        serializer.setSameSite("Strict");
        // VULNERABLE - Not setting secure flag
        // serializer.setUseSecureCookie(true);  // MISSING!

        return serializer;
    }
}

Why this is vulnerable:

  • The serializer sets HttpOnly and SameSite but never calls setUseSecureCookie(true)
  • Spring Session then writes the session cookie without a Secure attribute, so it is sent over HTTP
// VULNERABLE - Micronaut application with insecure cookies
package com.example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.*;
import io.micronaut.http.cookie.Cookie;
import java.util.UUID;

@Controller("/auth")
public class MicronautAuthController {

    @Post("/login")
    public HttpResponse<?> login(
            @Body LoginRequest request) {

        if (authenticateUser(request.getUsername(), request.getPassword())) {
            String sessionToken = UUID.randomUUID().toString();

            // VULNERABLE - Cookie without secure flag
            Cookie cookie = Cookie.of("session_id", sessionToken)
                .maxAge(3600)
                .httpOnly(true)
                .path("/");
                // Missing: .secure(true)

            return HttpResponse.ok()
                .body("{\"status\": \"logged_in\"}")
                .cookie(cookie);
        }

        return HttpResponse.unauthorized()
            .body("{\"error\": \"Invalid credentials\"}");
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }

    static class LoginRequest {
        private String username;
        private String password;

        public String getUsername() { return username; }
        public void setUsername(String username) { this.username = username; }
        public String getPassword() { return password; }
        public void setPassword(String password) { this.password = password; }
    }
}

Why this is vulnerable:

  • The builder chain sets maxAge, httpOnly and path but stops short of .secure(true)
  • Micronaut defaults the flag to off, so it has to be set explicitly on every cookie
  • The session token is sent over HTTP and can be replayed

Secure Patterns

SecureLoginServlet.java
// SECURE - Servlet cookie with proper security flags
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.Base64;

@WebServlet("/secure-login")
public class SecureLoginServlet extends HttpServlet {

    private static final SecureRandom secureRandom = new SecureRandom();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticateUser(username, password)) {
            String sessionToken = generateSecureToken();

            // SECURE - Cookie with all critical security flags.
            // Name it something other than JSESSIONID: that name belongs to the
            // container's own session tracking cookie, and setting it by hand
            // collides with whatever the container emits for getSession().
            Cookie sessionCookie = new Cookie("app_session", sessionToken);
            sessionCookie.setSecure(true);      // HTTPS only
            sessionCookie.setHttpOnly(true);    // Not accessible via JavaScript
            sessionCookie.setPath("/");
            sessionCookie.setMaxAge(3600);      // 1 hour
            sessionCookie.setAttribute("SameSite", "Strict");  // Servlet 6.0+

            response.addCookie(sessionCookie);

            response.setContentType("application/json");
            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private String generateSecureToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }

    private boolean authenticateUser(String username, String password) {
        // Secure authentication logic (bcrypt, etc.)
        return true;
    }
}

Why this works:

  • Secure + HttpOnly restrict cookie transport and script access.
  • SameSite=Strict blocks cross-site sending.
  • Strong tokens and short lifetimes reduce exposure.

The cookie name matters as much as the flags. An application-issued cookie named JSESSIONID is not the container's session cookie and does not inherit <cookie-config>; the container still creates and sends its own the first time anything calls request.getSession(), and the two overwrite each other in an order nothing in the application controls. Give application cookies their own names and secure them here, and secure the container's JSESSIONID through web.xml - the two mechanisms do not substitute for one another.

application.properties
# SECURE configuration
server.port=8443
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=${SSL_KEYSTORE_PASSWORD}
server.ssl.key-store-type=PKCS12

# SECURE - Session cookie security
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.same-site=strict
server.servlet.session.timeout=30m
SecurityConfig.java
package com.example.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecureSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .requiresChannel(channel -> channel
                .anyRequest().requiresSecure()  // Force HTTPS
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .sessionManagement(session -> session
                .sessionFixation().newSession()
                .maximumSessions(1)
            );

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}
LoginController.java
package com.example.controller;

import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import jakarta.servlet.http.HttpServletResponse;
import java.security.SecureRandom;
import java.time.Duration;
import java.util.Base64;

@RestController
@RequestMapping("/api")
public class SecureLoginController {

    private static final SecureRandom secureRandom = new SecureRandom();

    @PostMapping("/login")
    public LoginResponse login(
            @RequestParam String username,
            @RequestParam String password,
            HttpServletResponse response) {

        if (authenticateUser(username, password)) {
            String token = generateSecureToken();

            // SECURE - ResponseCookie serialises the whole Set-Cookie header,
            // so every attribute is set in one place and none can be forgotten
            ResponseCookie authCookie = ResponseCookie.from("auth_token", token)
                    .secure(true)       // HTTPS only
                    .httpOnly(true)     // Not accessible via JavaScript
                    .sameSite("Strict")
                    .path("/")
                    .maxAge(Duration.ofHours(1))
                    .build();

            response.addHeader(HttpHeaders.SET_COOKIE, authCookie.toString());

            return new LoginResponse("logged_in", username);
        }

        throw new RuntimeException("Invalid credentials");
    }

    private String generateSecureToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }

    private boolean authenticateUser(String username, String password) {
        // Secure authentication (BCrypt, etc.)
        return true;
    }

    static class LoginResponse {
        public String status;
        public String username;

        public LoginResponse(String status, String username) {
            this.status = status;
            this.username = username;
        }
    }
}

Why this works:

  • App-wide session cookie flags + HTTPS enforcement reduce misconfig risk.
  • Session fixation protection limits reuse of old identifiers.
  • Custom cookies add Secure/HttpOnly/SameSite with strong tokens.
// SECURE - JAX-RS REST API with secure cookies
package com.example.api;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;

@Path("/auth")
public class SecureAuthResource {

    private static final SecureRandom secureRandom = new SecureRandom();

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(
            @FormParam("username") String username,
            @FormParam("password") String password) {

        if (authenticateUser(username, password)) {
            String sessionToken = generateSecureToken();

            // SECURE - NewCookie.Builder: Jakarta REST 3.1+
            NewCookie cookie = new NewCookie.Builder("session_id")
                .value(sessionToken)
                .path("/")
                .maxAge(3600)
                .secure(true)      // HTTPS only
                .httpOnly(true)    // Not accessible via JavaScript
                .sameSite(NewCookie.SameSite.STRICT)  // also Jakarta REST 3.1+
                .build();

            return Response.ok()
                .entity("{\"status\": \"logged_in\"}")
                .cookie(cookie)
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("{\"error\": \"Invalid credentials\"}")
            .build();
    }

    @POST
    @Path("/logout")
    public Response logout() {
        // SECURE - Delete cookie with same security settings
        NewCookie deleteCookie = new NewCookie.Builder("session_id")
            .value("")
            .path("/")
            .maxAge(0)
            .secure(true)
            .httpOnly(true)
            .build();

        return Response.ok()
            .entity("{\"status\": \"logged_out\"}")
            .cookie(deleteCookie)
            .build();
    }

    private String generateSecureToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }
}

Why this works:

  • Builder enforces Secure/HttpOnly/SameSite flags at creation time.
  • Logout deletes cookies with matching attributes.
  • Strong tokens and expirations reduce exposure.

NewCookie.Builder and NewCookie.SameSite both arrived in Jakarta REST 3.1, which is also where the positional NewCookie(...) constructors used in the vulnerable example above were deprecated for removal. On an older runtime the builder is unavailable and the seven-argument constructor is the only route - pass true for secure there, and set SameSite with a ContainerResponseFilter that rewrites the Set-Cookie header.

JSP/Servlet Web.xml Configuration

web.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- SECURE configuration -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
                             https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">

    <!-- SECURE - Session configuration -->
    <session-config>
        <session-timeout>30</session-timeout>
        <cookie-config>
            <!-- cookie-config is an ordered sequence: http-only precedes
                 secure, which precedes max-age and attribute -->
            <http-only>true</http-only>     <!-- XSS protection -->
            <secure>true</secure>           <!-- HTTPS only -->
            <!-- SameSite has no dedicated element; Servlet 6.0 adds the
                 generic <attribute> pair, which is the portable route -->
            <attribute>
                <attribute-name>SameSite</attribute-name>
                <attribute-value>Strict</attribute-value>
            </attribute>
        </cookie-config>
        <tracking-mode>COOKIE</tracking-mode>
    </session-config>

    <!-- SECURE - Security constraints -->
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>Protected Area</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

</web-app>
SecureSessionServlet.java
package com.example.servlet;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/secure-session-login")
public class SecureSessionServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        if (authenticateUser(username, password)) {
            // SECURE - Session cookie inherits web.xml config (secure flag enabled)
            HttpSession session = request.getSession(true);
            session.setAttribute("username", username);
            session.setAttribute("authenticated", true);
            session.setMaxInactiveInterval(1800);  // 30 minutes

            // Prevent session fixation
            request.changeSessionId();

            response.setContentType("application/json");
            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }
}

Why this works:

  • Centralized web.xml flags secure all container-managed cookies.
  • Transport guarantee forces HTTPS at the container level.
  • Session fixation protection rotates IDs after login.

<secure> and <http-only> are the only two cookie attributes the deployment descriptor has ever had dedicated elements for. There is no <same-site> element in any version of the schema, and adding one fails deployment on schema validation rather than being ignored - Servlet 6.0's generic <attribute> pair is what carries SameSite (and anything else) through the descriptor. On Servlet 5.0 and earlier the descriptor cannot express it at all, and SameSite has to come from the container: Tomcat's CookieProcessor sameSiteCookies setting in context.xml, or a response filter.

Secure Remember-Me Implementation

// SECURE - Remember-me with proper security
package com.example.auth;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

@WebServlet("/secure-remember-login")
public class SecureRememberMeServlet extends HttpServlet {

    private static final SecureRandom secureRandom = new SecureRandom();

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException {

        String username = request.getParameter("username");
        String password = request.getParameter("password");
        boolean rememberMe = "true".equals(request.getParameter("remember_me"));

        if (authenticateUser(username, password)) {
            String sessionToken = generateSecureToken();

            // SECURE - Session cookie with all flags
            response.addCookie(createSecureCookie("session_id", sessionToken, 3600));

            if (rememberMe) {
                String rememberToken = generateSecureToken();

                // Store hashed token in database
                String tokenHash = hashToken(rememberToken);
                storeRememberToken(username, tokenHash);

                // SECURE - Remember-me cookie with all flags
                response.addCookie(createSecureCookie(
                    "remember_me",
                    rememberToken,
                    30 * 24 * 3600  // 30 days
                ));
            }

            response.setContentType("application/json");
            response.getWriter().write("{\"status\": \"logged_in\"}");
        } else {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json");
            response.getWriter().write("{\"error\": \"Invalid credentials\"}");
        }
    }

    private Cookie createSecureCookie(String name, String value, int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setSecure(true);      // HTTPS only
        cookie.setHttpOnly(true);    // Not accessible via JavaScript
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        cookie.setAttribute("SameSite", "Strict");  // Servlet 6.0+
        return cookie;
    }

    private String generateSecureToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
    }

    private String hashToken(String token) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hash = digest.digest(token.getBytes());
            return Base64.getEncoder().encodeToString(hash);
        } catch (Exception e) {
            throw new RuntimeException("Hashing failed", e);
        }
    }

    private boolean authenticateUser(String username, String password) {
        return true;
    }

    private void storeRememberToken(String username, String tokenHash) {
        // Database storage (store hash, not plaintext)
    }
}

Why this works:

  • Secure/HttpOnly/SameSite are enforced consistently for long-lived cookies.
  • Token hashing limits impact of database compromise.
  • Short session lifetimes reduce exposure.

Spring Session With Secure Configuration

// SECURE - Spring Session with proper cookie configuration
package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class SecureSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();

        serializer.setCookieName("SESSIONID");
        serializer.setCookiePath("/");
        serializer.setCookieMaxAge(1800);  // 30 minutes

        // SECURE - All security flags enabled
        serializer.setUseSecureCookie(true);     // HTTPS only
        serializer.setUseHttpOnlyCookie(true);   // XSS protection
        serializer.setSameSite("Strict");        // CSRF defense-in-depth

        return serializer;
    }
}

Why this works:

  • Secure/HttpOnly/SameSite are all set on the one serializer that writes the cookie.
  • The Secure flag keeps the session ID off plain HTTP.
  • A 30-minute expiry limits exposure.

Setting SameSite Before Servlet 6.0

// Spring's ResponseCookie builds the header itself, so SameSite is
// available without waiting for Servlet 6.0 or hand-formatting the
// Set-Cookie string.
ResponseCookie cookie = ResponseCookie.from("SESSION", value)
        .secure(true)
        .httpOnly(true)
        .sameSite("Strict")
        .path("/")
        .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

Why this works: ResponseCookie serialises the whole header, so it is not limited to the attributes the Servlet Cookie class exposes. On Servlet 6.0+ Cookie.setAttribute("SameSite", ...) is available directly.

Considerations

Not every cookie needs the full attribute set. Session identifiers, authentication and remember-me tokens, CSRF tokens, and anything carrying user identity do. A cookie holding a UI preference such as a collapsed sidebar or a chosen theme does not, and flagging it is not this finding - close it with the reason recorded rather than marking every cookie in the application.

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.

The deployment can strip the flags you set. A Secure cookie is not sent over plain HTTP at all, so the attribute is only as good as the TLS path. Where a reverse proxy or load balancer terminates TLS, the container sees a plain HTTP request unless the proxy forwards X-Forwarded-Proto and the application is configured to trust it - server.forward-headers-strategy in Spring Boot. Get that wrong and the cookie is either dropped in production or the application believes an insecure request was secure. Verify against the deployed environment, not a local HTTPS run.

Testing

  • Inspect the actual Set-Cookie response headers rather than the code that sets them - curl -I https://host/path or the browser's network panel. A configuration property that looks correct can be overridden by a filter, a proxy, or a second cookie-setting call later in the same request.
  • Confirm every sensitive cookie carries Secure, HttpOnly and a SameSite value, not just the one the finding named. Session, authentication and CSRF cookies are often set in different places.
  • Sign in over HTTPS, then request the application over plain HTTP and confirm the browser sends no session or authentication cookie in the request. Secure governs what the browser transmits, not what the server emits - a server will happily write Set-Cookie: ...; Secure on an HTTP response, so checking the response alone proves nothing.
  • Test through the real ingress path, including any reverse proxy, so that TLS termination and X-Forwarded-Proto handling are exercised as deployed.

Common Pitfalls

  • Setting server.servlet.session.cookie.secure=true in Spring Boot, which only governs the container-managed session cookie, while a custom Cookie/NewCookie built manually in a controller (auth token, remember-me, CSRF) still needs .setSecure(true)/.secure(true) called on that specific object - the global session property doesn't apply to cookies your own code constructs.
  • Configuring <secure>true</secure> in web.xml's <cookie-config>, which applies only to the container's session tracking cookie (JSESSIONID) - JAX-RS NewCookie instances and manually created jakarta.servlet.http.Cookie objects elsewhere in the same application are unaffected and default to non-secure unless set individually.
  • Relying on <transport-guarantee>CONFIDENTIAL</transport-guarantee> in a <security-constraint> to "handle HTTPS" - this forces the connection itself to HTTPS (redirecting HTTP requests) but does not set the Secure attribute on any cookie; a cookie without secure=true is still sent over HTTP wherever the constraint's URL pattern doesn't apply.

Additional Resources