From d824832126016de2da48341a567a37bf8d5e82f0 Mon Sep 17 00:00:00 2001 From: kogun Date: Fri, 28 Aug 2026 09:15:22 +0300 Subject: [PATCH] Avoid int overflow in expiration time calculations FlashMap.startExpirationPeriod(int) and MockMvcWebConnection's cookie handling both multiplied an int number of seconds by 1000 without widening to long. Above 2_147_483 seconds (about 24.9 days) the multiplication overflows to a negative offset, so the computed expiration time lands in the past. For FlashMap, a flash map configured through AbstractFlashMapManager.setFlashMapTimeout(int) with a large timeout is then treated as expired immediately. For MockMvcWebConnection, a cookie with a large max-age is removed from the CookieManager instead of being stored. This applies the same widening already used for this pattern in gh-25613. Signed-off-by: kogun --- .../htmlunit/MockMvcWebConnection.java | 2 +- .../MockMvcWebClientBuilderTests.java | 20 +++++++++++++++++++ .../springframework/web/servlet/FlashMap.java | 2 +- .../web/servlet/FlashMapTests.java | 9 +++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java index 2d57576cd3f9..69af8e958f84 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -183,7 +183,7 @@ private void storeCookies(WebRequest webRequest, jakarta.servlet.http.Cookie[] c private static Cookie createCookie(jakarta.servlet.http.Cookie cookie) { Date expires = null; if (cookie.getMaxAge() > -1) { - expires = new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000); + expires = new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000L); } BasicClientCookie result = new BasicClientCookie(cookie.getName(), cookie.getValue()); result.setDomain(cookie.getDomain()); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java index 610413675ea9..bce9acb27396 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilderTests.java @@ -109,6 +109,17 @@ void cookiesAreManaged() throws Exception { assertThat(getResponse(client, "http://localhost/").getContentAsString()).isEqualTo("NA"); } + @Test + void cookieWithLargeMaxAgeIsStored() throws Exception { + this.mockMvc = MockMvcBuilders.standaloneSetup(new CookieController()).build(); + WebClient client = MockMvcWebClientBuilder.mockMvcSetup(this.mockMvc).build(); + + assertThat(getResponse(client, "http://localhost/").getContentAsString()).isEqualTo("NA"); + assertThat(postResponse(client, "http://localhost/long-lived", "cookie=foo") + .getContentAsString()).isEqualTo("Set"); + assertThat(getResponse(client, "http://localhost/").getContentAsString()).isEqualTo("foo"); + } + private void assertMockMvcUsed(WebClient client, String url) throws Exception { assertThat(getResponse(client, url).getContentAsString()).isEqualTo("mvc"); } @@ -162,6 +173,15 @@ String setCookie(@RequestParam String cookie, HttpServletResponse response) { return "Set"; } + @PostMapping(path = "/long-lived", produces = "text/plain") + String setLongLivedCookie(@RequestParam String cookie, HttpServletResponse response) { + jakarta.servlet.http.Cookie longLived = new jakarta.servlet.http.Cookie(COOKIE_NAME, cookie); + longLived.setMaxAge(Integer.MAX_VALUE); + longLived.setPath("/"); + response.addCookie(longLived); + return "Set"; + } + @DeleteMapping(path = "/", produces = "text/plain") String deleteCookie(HttpServletResponse response) { jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie(COOKIE_NAME, ""); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java index 9abc282e4598..fb50e8d6a6b4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java @@ -112,7 +112,7 @@ public MultiValueMap getTargetRequestParams() { * @param timeToLive the number of seconds before expiration */ public void startExpirationPeriod(int timeToLive) { - this.expirationTime = System.currentTimeMillis() + timeToLive * 1000; + this.expirationTime = System.currentTimeMillis() + timeToLive * 1000L; } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/FlashMapTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/FlashMapTests.java index dedbc294744d..6e038dc09d92 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/FlashMapTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/FlashMapTests.java @@ -50,6 +50,15 @@ void notExpired() throws InterruptedException { assertThat(flashMap.isExpired()).isFalse(); } + @Test + void notExpiredWithLargeTimeToLive() { + FlashMap flashMap = new FlashMap(); + flashMap.startExpirationPeriod(Integer.MAX_VALUE); + + assertThat(flashMap.getExpirationTime()).isGreaterThan(System.currentTimeMillis()); + assertThat(flashMap.isExpired()).isFalse(); + } + @Test void compareTo() { FlashMap flashMap1 = new FlashMap();