This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 707457f58 WW-5841 fix(boot): restrict session tracking to cookies and 
set SameSite=Lax (#2117)
707457f58 is described below

commit 707457f584072113d5c03d5e32d0c481cb20f6a5
Author: g0w6y <[email protected]>
AuthorDate: Sun Nov 8 20:28:17 2026 +0530

    WW-5841 fix(boot): restrict session tracking to cookies and set 
SameSite=Lax (#2117)
    
    * WW-5841 Restrict Struts Boot to cookie-only session tracking and add 
SameSite=Lax
    
    Without a web.xml, the embedded Tomcat context defaulted to COOKIE+URL
    session tracking, so a session id supplied only via ";jsessionid=" in the
    URL was honoured with no fresh Set-Cookie -- a session-fixation vector.
    Live-reproduced before this fix: a session established by cookie was resumed
    by replaying the id via the URL parameter with no Cookie header sent at all.
    
    Restricts session tracking to COOKIE only, and the session cookie now
    carries SameSite=Lax (previously unset). Secure is deliberately not forced:
    Boot opens a plain HTTP listener by default, and an operator terminating TLS
    in front of it sets Secure themselves, as with any other servlet container.
    
    Boot is part of 7.5.0 (unreleased, no production deployments yet), so this
    lands before anyone is exposed to it.
    
    Tests: StrutsBootTomcatTest gains testSessionCookieHasSameSiteLax and a
    URL-supplied-session-id-is-not-honoured test, with a cookie-based-session-
    still-works control. Full boot suite: 52/0 failures.
    
    * WW-5841 Address review: single session-tracking method, shorter comments, 
threat model bullets
    
    - fold the tracking-mode initializer and the SameSite cookie processor into
      one method, restrictSessionTracking(Context)
    - drop the Secure claim from the Javadoc and reduce the comments to what the
      code does
    - reduce the test Javadoc and drop the narrating comment lines
    - THREAT_MODEL.md section 5: add the cookie-only tracking and SameSite=Lax
      defaults to the Boot container defaults list
---
 THREAT_MODEL.md                                    |  2 ++
 .../org/apache/struts2/boot/StrutsBootServer.java  | 18 +++++++++++
 .../apache/struts2/boot/StrutsBootTomcatTest.java  | 35 ++++++++++++++++++++++
 .../org/apache/struts2/boot/fixture/HelloApp.java  |  1 +
 .../{HelloApp.java => SessionProbeAction.java}     | 34 ++++++++++++++-------
 .../templates/sessionProbe.ftl}                    | 22 ++------------
 6 files changed, 82 insertions(+), 30 deletions(-)

diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index d11007a1f..f6a8521aa 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -238,6 +238,8 @@ documented anti-pattern is `OUT-OF-MODEL`. 
*(documented/inferred — §14 Q-defa
   - error responses render without exception report or server version
   - `DefaultServlet` serves only an empty per-run docBase
   - Tomcat's base directory is a fresh owner-only temp directory, deleted on 
shutdown
+  - session tracking is cookie-only
+  - the session cookie carries `SameSite=Lax`
   - `devMode` is never switched on
 
   A deviation from these defaults is a Struts finding. Anything the developer 
adds on
diff --git a/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java 
b/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java
index d73b21ed7..46935e34b 100644
--- a/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java
+++ b/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java
@@ -20,6 +20,7 @@ package org.apache.struts2.boot;
 
 import jakarta.servlet.FilterConfig;
 import jakarta.servlet.ServletException;
+import jakarta.servlet.SessionTrackingMode;
 import org.apache.catalina.Context;
 import org.apache.catalina.LifecycleState;
 import org.apache.catalina.connector.Connector;
@@ -30,6 +31,7 @@ import org.apache.struts2.config.ConfigurationException;
 import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter;
 import org.apache.tomcat.util.descriptor.web.FilterDef;
 import org.apache.tomcat.util.descriptor.web.FilterMap;
+import org.apache.tomcat.util.http.Rfc6265CookieProcessor;
 
 import java.io.File;
 import java.io.IOException;
@@ -39,6 +41,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.attribute.PosixFilePermissions;
 import java.util.Comparator;
+import java.util.EnumSet;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicReference;
@@ -71,6 +74,7 @@ public final class StrutsBootServer implements AutoCloseable {
             hideErrorReportDetails(tomcat);
             Path docBase = Files.createDirectory(baseDir.resolve("webapp"));
             Context context = tomcat.addContext(settings.contextPath(), 
docBase.toString());
+            restrictSessionTracking(context);
             context.addServletContainerInitializer(
                     (classes, servletContext) -> 
servletContext.setAttribute(ActionRegistry.SERVLET_CONTEXT_ATTRIBUTE, registry),
                     null);
@@ -177,6 +181,20 @@ public final class StrutsBootServer implements 
AutoCloseable {
         }
     }
 
+    /**
+     * Restricts the embedded context to cookie-only session tracking with 
{@code SameSite=Lax};
+     * Secure is left to the operator's TLS setup.
+     */
+    private static void restrictSessionTracking(Context context) {
+        // Tomcat defaults to COOKIE+URL when no web.xml sets <tracking-mode>; 
keep cookies only.
+        context.addServletContainerInitializer(
+                (classes, servletContext) -> 
servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.COOKIE)),
+                null);
+        Rfc6265CookieProcessor cookieProcessor = new Rfc6265CookieProcessor();
+        cookieProcessor.setSameSiteCookies("lax");
+        context.setCookieProcessor(cookieProcessor);
+    }
+
     private static void hideErrorReportDetails(Tomcat tomcat) {
         ErrorReportValve valve = new ErrorReportValve();
         valve.setShowReport(false);
diff --git 
a/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java 
b/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java
index c3009eb25..d693b3fb9 100644
--- a/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java
+++ b/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java
@@ -125,6 +125,41 @@ public class StrutsBootTomcatTest {
         assertThat(response.body()).doesNotContain("at 
org.apache").doesNotContain("Apache Tomcat/");
     }
 
+    /** The session cookie carries {@code SameSite=Lax}. */
+    @Test
+    public void testSessionCookieHasSameSiteLax() throws Exception {
+        HttpResponse<String> response = get(server.baseUrl() + 
"/sessionProbe.action");
+
+        
assertThat(response.headers().firstValue("Set-Cookie")).hasValueSatisfying(cookie
 ->
+                assertThat(cookie).contains("SameSite=Lax"));
+    }
+
+    /** A session id supplied only in the URL is not resumed; the cookie-based 
flow still is. */
+    @Test
+    public void testUrlSuppliedSessionIdIsNotHonored() throws Exception {
+        HttpResponse<String> first = get(server.baseUrl() + 
"/sessionProbe.action");
+        assertThat(first.body()).contains("COUNTER=1");
+        String sessionId = extractSessionId(first);
+
+        HttpResponse<String> viaUrl = get(server.baseUrl() + 
"/sessionProbe.action;jsessionid=" + sessionId);
+        assertThat(viaUrl.body()).contains("COUNTER=1");
+        assertThat(extractSessionId(viaUrl)).isNotEqualTo(sessionId);
+
+        // Control: the ordinary cookie-based flow still resumes the same 
session.
+        HttpResponse<String> viaCookie = CLIENT.send(
+                HttpRequest.newBuilder(URI.create(server.baseUrl() + 
"/sessionProbe.action"))
+                        .header("Cookie", "JSESSIONID=" + sessionId)
+                        .GET().build(),
+                HttpResponse.BodyHandlers.ofString());
+        assertThat(viaCookie.body()).contains("COUNTER=2");
+    }
+
+    private static String extractSessionId(HttpResponse<String> response) {
+        String setCookie = response.headers().firstValue("Set-Cookie")
+                .orElseThrow(() -> new AssertionError("Expected a Set-Cookie 
header"));
+        return setCookie.substring("JSESSIONID=".length(), 
setCookie.indexOf(';'));
+    }
+
     @Test
     public void testAddressRestrictsListeningInterface() throws Exception {
         try (StrutsBootServer local = StrutsBoot.start(HelloApp.class, 
"--struts.boot.port=0", "--struts.boot.address=127.0.0.1")) {
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
index 541b5a6e8..0edcc305e 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
@@ -29,6 +29,7 @@ public class HelloApp implements StrutsBootApplication {
     public void actions(ActionRegistry actions) {
         actions.action("hello", HelloAction.class).success("hello.ftl");
         actions.action("boom", ThrowingAction.class).success("hello.ftl");
+        actions.action("sessionProbe", 
SessionProbeAction.class).success("sessionProbe.ftl");
         actions.namespace("/admin", admin -> {
             admin.action("users", 
UserAction.class).method("list").success("admin/users.ftl");
             admin.action("saveUser", 
UserAction.class).method("save").httpMethods(POST).redirectAction("success", 
"users");
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/SessionProbeAction.java
similarity index 51%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
copy to 
boot/src/test/java/org/apache/struts2/boot/fixture/SessionProbeAction.java
index 541b5a6e8..1e2be1b5a 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/SessionProbeAction.java
@@ -18,20 +18,32 @@
  */
 package org.apache.struts2.boot.fixture;
 
-import org.apache.struts2.boot.ActionRegistry;
-import org.apache.struts2.boot.StrutsBootApplication;
+import org.apache.struts2.action.SessionAware;
 
-import static org.apache.struts2.boot.HttpMethods.POST;
+import java.util.Map;
 
-public class HelloApp implements StrutsBootApplication {
+/**
+ * Forces an {@link jakarta.servlet.http.HttpSession} to exist and reports a 
counter stored in
+ * it, so a test can tell whether two requests landed in the *same* session 
(counter keeps
+ * incrementing) or different ones (counter resets to 1).
+ */
+public class SessionProbeAction implements SessionAware {
+
+    private Map<String, Object> session;
+
+    public String execute() {
+        Object current = session.get("probeCounter");
+        int next = (current instanceof Integer i ? i : 0) + 1;
+        session.put("probeCounter", next);
+        return "success";
+    }
+
+    public String getCounter() {
+        return String.valueOf(session.get("probeCounter"));
+    }
 
     @Override
-    public void actions(ActionRegistry actions) {
-        actions.action("hello", HelloAction.class).success("hello.ftl");
-        actions.action("boom", ThrowingAction.class).success("hello.ftl");
-        actions.namespace("/admin", admin -> {
-            admin.action("users", 
UserAction.class).method("list").success("admin/users.ftl");
-            admin.action("saveUser", 
UserAction.class).method("save").httpMethods(POST).redirectAction("success", 
"users");
-        });
+    public void withSession(Map<String, Object> session) {
+        this.session = session;
     }
 }
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java 
b/boot/src/test/resources/templates/sessionProbe.ftl
similarity index 51%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
copy to boot/src/test/resources/templates/sessionProbe.ftl
index 541b5a6e8..db042437f 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
+++ b/boot/src/test/resources/templates/sessionProbe.ftl
@@ -1,3 +1,4 @@
+<#--
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -16,22 +17,5 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.struts2.boot.fixture;
-
-import org.apache.struts2.boot.ActionRegistry;
-import org.apache.struts2.boot.StrutsBootApplication;
-
-import static org.apache.struts2.boot.HttpMethods.POST;
-
-public class HelloApp implements StrutsBootApplication {
-
-    @Override
-    public void actions(ActionRegistry actions) {
-        actions.action("hello", HelloAction.class).success("hello.ftl");
-        actions.action("boom", ThrowingAction.class).success("hello.ftl");
-        actions.namespace("/admin", admin -> {
-            admin.action("users", 
UserAction.class).method("list").success("admin/users.ftl");
-            admin.action("saveUser", 
UserAction.class).method("save").httpMethods(POST).redirectAction("success", 
"users");
-        });
-    }
-}
+-->
+COUNTER=<@s.property value="counter"/>

Reply via email to