From 9ee17ab716a4f1665175e21b41a3da92cc57abeb Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:35:46 +0000 Subject: [PATCH 1/4] Re-enable CSRF protection, add tokens to forms, SameSite=Lax session cookie Co-Authored-By: james.scarr --- .../bankapp/config/SecurityConfig.java | 3 +- src/main/resources/application.properties | 4 + src/main/resources/templates/dashboard.html | 15 ++- src/main/resources/templates/login.html | 2 +- src/main/resources/templates/register.html | 2 +- .../resources/templates/transactions.html | 9 +- .../controller/BankControllerCsrfTest.java | 118 ++++++++++++++++++ 7 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 src/test/java/com/example/bankapp/controller/BankControllerCsrfTest.java diff --git a/src/main/java/com/example/bankapp/config/SecurityConfig.java b/src/main/java/com/example/bankapp/config/SecurityConfig.java index 4dbd1572..1505447a 100644 --- a/src/main/java/com/example/bankapp/config/SecurityConfig.java +++ b/src/main/java/com/example/bankapp/config/SecurityConfig.java @@ -27,7 +27,6 @@ public static PasswordEncoder passwordEncoder() { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http - .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(authz -> authz .requestMatchers("/register").permitAll() .anyRequest().authenticated() @@ -41,7 +40,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logout(logout -> logout .invalidateHttpSession(true) .clearAuthentication(true) - .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) + .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "POST")) .logoutSuccessUrl("/login?logout") .permitAll() ) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 08663a63..fad0b857 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,7 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect spring.jpa.show-sql=true + +# Session cookie hardening +server.servlet.session.cookie.same-site=lax +server.servlet.session.cookie.http-only=true diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 23a0cc7f..3d6aedb2 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -26,7 +26,8 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a { + .navbar-custom a, + .navbar-custom .btn-link { color: #ffffff !important; } @@ -117,7 +118,11 @@ @@ -141,7 +146,7 @@

Account Details

Deposit
-
+
@@ -157,7 +162,7 @@

Account Details

Withdraw
- +
@@ -173,7 +178,7 @@

Account Details

Transfer Money
- +
diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html index 7b40190d..412a68eb 100644 --- a/src/main/resources/templates/login.html +++ b/src/main/resources/templates/login.html @@ -105,7 +105,7 @@

Login

- +
diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html index e410b74e..7555094b 100644 --- a/src/main/resources/templates/register.html +++ b/src/main/resources/templates/register.html @@ -105,7 +105,7 @@

Register a New Account

- +
diff --git a/src/main/resources/templates/transactions.html b/src/main/resources/templates/transactions.html index 307bc5de..3e5c4392 100644 --- a/src/main/resources/templates/transactions.html +++ b/src/main/resources/templates/transactions.html @@ -26,7 +26,8 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a { + .navbar-custom a, + .navbar-custom .btn-link { color: #ffffff !important; } @@ -96,7 +97,11 @@ diff --git a/src/test/java/com/example/bankapp/controller/BankControllerCsrfTest.java b/src/test/java/com/example/bankapp/controller/BankControllerCsrfTest.java new file mode 100644 index 00000000..308c10c6 --- /dev/null +++ b/src/test/java/com/example/bankapp/controller/BankControllerCsrfTest.java @@ -0,0 +1,118 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.config.SecurityConfig; +import com.example.bankapp.model.Account; +import com.example.bankapp.service.AccountService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(BankController.class) +@Import(SecurityConfig.class) +class BankControllerCsrfTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AccountService accountService; + + private final Account account = new Account(); + + @BeforeEach + void setUp() { + account.setUsername("alice"); + account.setBalance(new BigDecimal("1000")); + when(accountService.findAccountByUsername("alice")).thenReturn(account); + } + + @Test + @WithMockUser(username = "alice") + void transferWithoutCsrfTokenIsForbidden() throws Exception { + mockMvc.perform(post("/transfer").param("toUsername", "mallory").param("amount", "500")) + .andExpect(status().isForbidden()); + verify(accountService, never()).transferAmount(any(), anyString(), any()); + } + + @Test + @WithMockUser(username = "alice") + void transferWithCsrfTokenSucceeds() throws Exception { + mockMvc.perform(post("/transfer").param("toUsername", "bob").param("amount", "500").with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + verify(accountService).transferAmount(account, "bob", new BigDecimal("500")); + } + + @Test + @WithMockUser(username = "alice") + void withdrawWithoutCsrfTokenIsForbidden() throws Exception { + mockMvc.perform(post("/withdraw").param("amount", "500")) + .andExpect(status().isForbidden()); + verify(accountService, never()).withdraw(any(), any()); + } + + @Test + @WithMockUser(username = "alice") + void withdrawWithCsrfTokenSucceeds() throws Exception { + mockMvc.perform(post("/withdraw").param("amount", "500").with(csrf())) + .andExpect(redirectedUrl("/dashboard")); + verify(accountService).withdraw(account, new BigDecimal("500")); + } + + @Test + @WithMockUser(username = "alice") + void depositWithoutCsrfTokenIsForbidden() throws Exception { + mockMvc.perform(post("/deposit").param("amount", "500")) + .andExpect(status().isForbidden()); + verify(accountService, never()).deposit(any(), any()); + } + + @Test + @WithMockUser(username = "alice") + void depositWithCsrfTokenSucceeds() throws Exception { + mockMvc.perform(post("/deposit").param("amount", "500").with(csrf())) + .andExpect(redirectedUrl("/dashboard")); + verify(accountService).deposit(account, new BigDecimal("500")); + } + + @Test + @WithMockUser(username = "alice") + void dashboardFormsCarryCsrfToken() throws Exception { + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()) + .andExpect(content().string(org.hamcrest.Matchers.containsString("name=\"_csrf\""))); + } + + @Test + @WithMockUser(username = "alice") + void logoutViaGetIsNotHandled() throws Exception { + mockMvc.perform(get("/logout")) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(username = "alice") + void logoutViaPostWithCsrfTokenSucceeds() throws Exception { + mockMvc.perform(post("/logout").with(csrf())) + .andExpect(redirectedUrl("/login?logout")); + } +} From e1d18c5e94716e6bf2ad8fbaba459bd90f116b94 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:39:54 +0000 Subject: [PATCH 2/4] Keep Logout as a nav anchor that submits a hidden POST form Co-Authored-By: james.scarr --- src/main/resources/templates/dashboard.html | 8 +++----- src/main/resources/templates/transactions.html | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 3d6aedb2..0c1a44e4 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -26,8 +26,7 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a, - .navbar-custom .btn-link { + .navbar-custom a { color: #ffffff !important; } @@ -119,9 +118,8 @@ diff --git a/src/main/resources/templates/transactions.html b/src/main/resources/templates/transactions.html index 3e5c4392..d588cb7c 100644 --- a/src/main/resources/templates/transactions.html +++ b/src/main/resources/templates/transactions.html @@ -26,8 +26,7 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a, - .navbar-custom .btn-link { + .navbar-custom a { color: #ffffff !important; } @@ -98,9 +97,8 @@ From b5dc0b88ece0be8aff42e312354b9d43e962ea48 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:41:22 +0000 Subject: [PATCH 3/4] Add no-JS fallback submit button for POST logout Co-Authored-By: james.scarr --- src/main/resources/templates/dashboard.html | 10 +++++++--- src/main/resources/templates/transactions.html | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 0c1a44e4..4d69fc96 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -26,7 +26,8 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a { + .navbar-custom a, + .navbar-custom .btn-link { color: #ffffff !important; } @@ -107,6 +108,7 @@ text-decoration: underline; } + diff --git a/src/main/resources/templates/transactions.html b/src/main/resources/templates/transactions.html index d588cb7c..a28380ad 100644 --- a/src/main/resources/templates/transactions.html +++ b/src/main/resources/templates/transactions.html @@ -26,7 +26,8 @@ border-bottom: 2px solid #B08D57; } - .navbar-custom a { + .navbar-custom a, + .navbar-custom .btn-link { color: #ffffff !important; } @@ -86,6 +87,7 @@ text-decoration: underline; } + From 0d8e2473c8554a29d33602477fc969bf7d3d21f5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:51:53 +0000 Subject: [PATCH 4/4] Keep /logout href on the Logout nav link Co-Authored-By: james.scarr --- src/main/resources/templates/dashboard.html | 2 +- src/main/resources/templates/transactions.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 4d69fc96..1101535d 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -121,7 +121,7 @@ diff --git a/src/main/resources/templates/transactions.html b/src/main/resources/templates/transactions.html index a28380ad..85d4a28e 100644 --- a/src/main/resources/templates/transactions.html +++ b/src/main/resources/templates/transactions.html @@ -100,7 +100,7 @@