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
secureattribute - 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.xmlor 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
Servlet Cookie Without Secure Flag
// 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
// 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();
}
}
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 noSecureattribute - The custom
auth_tokencookie never callssetSecure(true)either - Both are sent over HTTP wherever the application answers on HTTP
JAX-RS Cookie Without Security
// 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:
NewCookiesecure parameter set tofalse- Cookie sent over HTTP, where the session token can be intercepted
- Whoever holds that token is authenticated to the REST API
JSP Session Cookie Misconfiguration
<?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>
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.xmlmissing<secure>true</secure>- The container-managed session cookie is transmitted over HTTP
- The fix belongs in the deployment descriptor, not in the servlet code
Remember-Me Cookie Without Secure Flag
// 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
Jakarta EE Cookie Without Security
// 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,PathandMaxAgebut notsetSecure(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
Spring Session Cookie Misconfiguration
// 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
HttpOnlyandSameSitebut never callssetUseSecureCookie(true) - Spring Session then writes the session cookie without a
Secureattribute, so it is sent over HTTP
Micronaut Cookie Without Security
// 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,httpOnlyandpathbut 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
Servlet Cookie With All Security Flags
// 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.
Spring Boot With Secure Cookie Configuration
# 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
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);
}
}
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.
JAX-RS Cookie With Security
// 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
<?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>
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-Cookieresponse headers rather than the code that sets them -curl -I https://host/pathor 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,HttpOnlyand aSameSitevalue, 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.
Securegoverns what the browser transmits, not what the server emits - a server will happily writeSet-Cookie: ...; Secureon 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-Protohandling are exercised as deployed.
Common Pitfalls
- Setting
server.servlet.session.cookie.secure=truein Spring Boot, which only governs the container-managed session cookie, while a customCookie/NewCookiebuilt 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>inweb.xml's<cookie-config>, which applies only to the container's session tracking cookie (JSESSIONID) - JAX-RSNewCookieinstances and manually createdjakarta.servlet.http.Cookieobjects 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 theSecureattribute on any cookie; a cookie withoutsecure=trueis still sent over HTTP wherever the constraint's URL pattern doesn't apply.
Additional Resources
- CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute
- Jakarta Servlet
CookieAPI -setAttribute(String, String), the Servlet 6.0+ route toSameSite - Java Servlet Cookie API (legacy
javax.servlet) - pre-Jakarta API, with noSameSitesupport - OWASP Session Management Cheat Sheet
- Spring Security - HTTP and proxy headers - why
X-Forwarded-Protohandling decides whether aSecurecookie is ever sent - Spring Security Documentation