From c00ca5a32eb68eb3fa772c91d4fa846060095034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Tue, 3 Mar 2026 09:19:05 +0100 Subject: [PATCH] feat(chore): added qodana analysis job --- .gitignore | 5 +++- .../security/AdminLoginThrottleService.java | 28 +++++++++++++------ .../security/AdminSessionService.java | 9 +++--- .../src/main/resources/application.properties | 1 + .../controller/AdminAuthSecurityTest.java | 3 +- .../AdminLoginThrottleServiceTest.java | 25 ++++++++++++++++- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 9381358..ab81c7a 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,7 @@ build/ ./storage_orders ./storage_quotes storage_orders -storage_quotes \ No newline at end of file +storage_quotes + +# Qodana local reports/artifacts +backend/.qodana/ diff --git a/backend/src/main/java/com/printcalculator/security/AdminLoginThrottleService.java b/backend/src/main/java/com/printcalculator/security/AdminLoginThrottleService.java index de2b5ad..1b95e81 100644 --- a/backend/src/main/java/com/printcalculator/security/AdminLoginThrottleService.java +++ b/backend/src/main/java/com/printcalculator/security/AdminLoginThrottleService.java @@ -1,6 +1,7 @@ package com.printcalculator.security; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Instant; @@ -14,6 +15,13 @@ public class AdminLoginThrottleService { private static final long MAX_DELAY_SECONDS = 3600L; private final ConcurrentHashMap attemptsByClient = new ConcurrentHashMap<>(); + private final boolean trustProxyHeaders; + + public AdminLoginThrottleService( + @Value("${admin.auth.trust-proxy-headers:false}") boolean trustProxyHeaders + ) { + this.trustProxyHeaders = trustProxyHeaders; + } public OptionalLong getRemainingLockSeconds(String clientKey) { LoginAttemptState state = attemptsByClient.get(clientKey); @@ -47,17 +55,19 @@ public class AdminLoginThrottleService { } public String resolveClientKey(HttpServletRequest request) { - String forwardedFor = request.getHeader("X-Forwarded-For"); - if (forwardedFor != null && !forwardedFor.isBlank()) { - String[] parts = forwardedFor.split(","); - if (parts.length > 0 && !parts[0].trim().isEmpty()) { - return parts[0].trim(); + if (trustProxyHeaders) { + String forwardedFor = request.getHeader("X-Forwarded-For"); + if (forwardedFor != null && !forwardedFor.isBlank()) { + String[] parts = forwardedFor.split(","); + if (parts.length > 0 && !parts[0].trim().isEmpty()) { + return parts[0].trim(); + } } - } - String realIp = request.getHeader("X-Real-IP"); - if (realIp != null && !realIp.isBlank()) { - return realIp.trim(); + String realIp = request.getHeader("X-Real-IP"); + if (realIp != null && !realIp.isBlank()) { + return realIp.trim(); + } } String remoteAddress = request.getRemoteAddr(); diff --git a/backend/src/main/java/com/printcalculator/security/AdminSessionService.java b/backend/src/main/java/com/printcalculator/security/AdminSessionService.java index 1ab99ef..2e797f8 100644 --- a/backend/src/main/java/com/printcalculator/security/AdminSessionService.java +++ b/backend/src/main/java/com/printcalculator/security/AdminSessionService.java @@ -24,6 +24,7 @@ import java.util.UUID; public class AdminSessionService { public static final String COOKIE_NAME = "admin_session"; + private static final String COOKIE_PATH = "/api/admin"; private static final String HMAC_ALGORITHM = "HmacSHA256"; private final ObjectMapper objectMapper; @@ -127,20 +128,20 @@ public class AdminSessionService { public ResponseCookie buildLoginCookie(String token) { return ResponseCookie.from(COOKIE_NAME, token) - .path("/") + .path(COOKIE_PATH) .httpOnly(true) .secure(true) - .sameSite("Lax") + .sameSite("Strict") .maxAge(Duration.ofMinutes(sessionTtlMinutes)) .build(); } public ResponseCookie buildLogoutCookie() { return ResponseCookie.from(COOKIE_NAME, "") - .path("/") + .path(COOKIE_PATH) .httpOnly(true) .secure(true) - .sameSite("Lax") + .sameSite("Strict") .maxAge(Duration.ZERO) .build(); } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ded5b09..d636a7e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -46,3 +46,4 @@ app.frontend.base-url=${APP_FRONTEND_BASE_URL:http://localhost:4200} admin.password=${ADMIN_PASSWORD} admin.session.secret=${ADMIN_SESSION_SECRET} admin.session.ttl-minutes=${ADMIN_SESSION_TTL_MINUTES:480} +admin.auth.trust-proxy-headers=${ADMIN_AUTH_TRUST_PROXY_HEADERS:false} diff --git a/backend/src/test/java/com/printcalculator/controller/AdminAuthSecurityTest.java b/backend/src/test/java/com/printcalculator/controller/AdminAuthSecurityTest.java index 6b82749..e5cd834 100644 --- a/backend/src/test/java/com/printcalculator/controller/AdminAuthSecurityTest.java +++ b/backend/src/test/java/com/printcalculator/controller/AdminAuthSecurityTest.java @@ -58,7 +58,8 @@ class AdminAuthSecurityTest { assertTrue(setCookie.contains("admin_session=")); assertTrue(setCookie.contains("HttpOnly")); assertTrue(setCookie.contains("Secure")); - assertTrue(setCookie.contains("SameSite=Lax")); + assertTrue(setCookie.contains("SameSite=Strict")); + assertTrue(setCookie.contains("Path=/api/admin")); } @Test diff --git a/backend/src/test/java/com/printcalculator/security/AdminLoginThrottleServiceTest.java b/backend/src/test/java/com/printcalculator/security/AdminLoginThrottleServiceTest.java index e7447e2..c8f0064 100644 --- a/backend/src/test/java/com/printcalculator/security/AdminLoginThrottleServiceTest.java +++ b/backend/src/test/java/com/printcalculator/security/AdminLoginThrottleServiceTest.java @@ -1,12 +1,15 @@ package com.printcalculator.security; +import jakarta.servlet.http.HttpServletRequest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class AdminLoginThrottleServiceTest { - private final AdminLoginThrottleService service = new AdminLoginThrottleService(); + private final AdminLoginThrottleService service = new AdminLoginThrottleService(false); @Test void registerFailure_ShouldDoubleDelay() { @@ -14,4 +17,24 @@ class AdminLoginThrottleServiceTest { assertEquals(4L, service.registerFailure("127.0.0.1")); assertEquals(8L, service.registerFailure("127.0.0.1")); } + + @Test + void resolveClientKey_ShouldUseRemoteAddress_WhenProxyHeadersAreNotTrusted() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("X-Forwarded-For")).thenReturn("203.0.113.10"); + when(request.getHeader("X-Real-IP")).thenReturn("203.0.113.11"); + when(request.getRemoteAddr()).thenReturn("10.0.0.5"); + + assertEquals("10.0.0.5", service.resolveClientKey(request)); + } + + @Test + void resolveClientKey_ShouldUseForwardedFor_WhenProxyHeadersAreTrusted() { + AdminLoginThrottleService trustedService = new AdminLoginThrottleService(true); + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getHeader("X-Forwarded-For")).thenReturn("203.0.113.10, 10.0.0.5"); + when(request.getRemoteAddr()).thenReturn("10.0.0.5"); + + assertEquals("203.0.113.10", trustedService.resolveClientKey(request)); + } }