From e41a47e5aeaec2dd38930a31aadc0813f87374a6 Mon Sep 17 00:00:00 2001 From: 5Amogh Date: Wed, 12 Nov 2025 17:47:05 +0530 Subject: [PATCH 1/4] fix: amm-1927 http options vulnerability fixed through cors --- .../com/iemr/common/config/CorsConfig.java | 15 ++- .../utils/JwtUserIdValidationFilter.java | 113 ++++++++++++++++-- .../utils/http/HTTPRequestInterceptor.java | 37 +++++- 3 files changed, 151 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/iemr/common/config/CorsConfig.java b/src/main/java/com/iemr/common/config/CorsConfig.java index 2e226b79..f711eb49 100644 --- a/src/main/java/com/iemr/common/config/CorsConfig.java +++ b/src/main/java/com/iemr/common/config/CorsConfig.java @@ -12,6 +12,19 @@ public class CorsConfig implements WebMvcConfigurer { @Value("${cors.allowed-origins}") private String allowedOrigins; + /** + * Spring MVC CORS configuration (framework level). + * + * NOTE: This configuration is permissive at the Spring framework level. + * Actual granular CORS enforcement (origin validation, endpoint-specific method control) + * is handled by JwtUserIdValidationFilter, which implements a two-layer security approach: + * + * 1. Spring CORS config: Permissive at framework level (allows PUT/DELETE for all endpoints) + * 2. JwtUserIdValidationFilter: Enforces strict origin validation and endpoint-specific method restrictions + * + * This design allows Spring to handle CORS preflight requests, while the filter enforces + * security policies before requests reach controllers. + */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -20,7 +33,7 @@ public void addCorsMappings(CorsRegistry registry) { .map(String::trim) .toArray(String[]::new)) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") + .allowedHeaders("Authorization", "Content-Type", "Accept", "Jwttoken") .exposedHeaders("Authorization", "Jwttoken") .allowCredentials(true) .maxAge(3600); diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 16466ee5..f708c633 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -2,6 +2,10 @@ import java.io.IOException; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +27,21 @@ public class JwtUserIdValidationFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private final String allowedOrigins; + // Default allowed methods for unconfigured endpoints + private static final Set DEFAULT_ALLOWED_METHODS = Set.of("GET", "POST", "OPTIONS"); + + // Endpoint-specific method control map + // Key = endpoint path pattern (supports wildcards), Value = Set of allowed HTTP methods + private static final Map> ENDPOINT_ALLOWED_METHODS = new HashMap<>(); + + static { + Set dynamicFormMethods = new HashSet<>(); + dynamicFormMethods.add("GET"); + dynamicFormMethods.add("POST"); + dynamicFormMethods.add("DELETE"); + ENDPOINT_ALLOWED_METHODS.put("/dynamicForm/delete/*/field", dynamicFormMethods); + } + public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, String allowedOrigins) { this.jwtAuthenticationUtil = jwtAuthenticationUtil; @@ -36,27 +55,68 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo HttpServletResponse response = (HttpServletResponse) servletResponse; String origin = request.getHeader("Origin"); + String method = request.getMethod(); + String uri = request.getRequestURI(); logger.debug("Incoming Origin: {}", origin); + logger.debug("Request Method: {}", method); + logger.debug("Request URI: {}", uri); logger.debug("Allowed Origins Configured: {}", allowedOrigins); - logger.info("Add server authorization header to response"); + + // STEP 1: STRICT Origin Validation - Block unauthorized origins immediately + // For OPTIONS requests, Origin header is required (CORS preflight) + if ("OPTIONS".equalsIgnoreCase(method)) { + if (origin == null) { + logger.warn("BLOCKED - OPTIONS request without Origin header | Method: {} | URI: {}", method, uri); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "OPTIONS request requires Origin header"); + return; + } + if (!isOriginAllowed(origin)) { + logger.warn("BLOCKED - Unauthorized Origin | Origin: {} | Method: {} | URI: {}", origin, method, uri); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Origin not allowed"); + return; + } + } else { + // For non-OPTIONS requests, validate origin if present + if (origin != null && !isOriginAllowed(origin)) { + logger.warn("BLOCKED - Unauthorized Origin | Origin: {} | Method: {} | URI: {}", origin, method, uri); + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Origin not allowed"); + return; + } + } + + // STEP 2: Endpoint-Specific Method Validation + String path = request.getRequestURI(); + String contextPath = request.getContextPath(); + String relativePath = path.startsWith(contextPath) ? path.substring(contextPath.length()) : path; + + Set allowedMethods = getAllowedMethodsForEndpoint(relativePath); + if (!allowedMethods.contains(method.toUpperCase())) { + logger.warn("BLOCKED - Method Not Allowed | Method: {} | URI: {} | Allowed Methods: {}", + method, uri, allowedMethods); + response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, + "Method " + method + " not allowed for this endpoint"); + return; + } + + // STEP 3: Add CORS Headers (only for validated origins) if (origin != null && isOriginAllowed(origin)) { - response.setHeader("Access-Control-Allow-Origin", origin); - response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Jwttoken, serverAuthorization, ServerAuthorization, serverauthorization, Serverauthorization"); + response.setHeader("Access-Control-Allow-Origin", origin); // Never use wildcard + response.setHeader("Access-Control-Allow-Methods", String.join(", ", allowedMethods) + ", OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", + "Authorization, Content-Type, Accept, Jwttoken, serverAuthorization, ServerAuthorization, serverauthorization, Serverauthorization"); response.setHeader("Access-Control-Allow-Credentials", "true"); - } else { - logger.warn("Origin [{}] is NOT allowed. CORS headers NOT added.", origin); + response.setHeader("Access-Control-Max-Age", "3600"); + logger.info("Origin Validated | Origin: {} | Method: {} | URI: {}", origin, method, uri); } - if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { - logger.info("OPTIONS request - skipping JWT validation"); + // STEP 4: Handle OPTIONS Preflight Request + if ("OPTIONS".equalsIgnoreCase(method)) { + logger.info("OPTIONS preflight request - skipping JWT validation"); response.setStatus(HttpServletResponse.SC_OK); return; } - String path = request.getRequestURI(); - String contextPath = request.getContextPath(); logger.info("JwtUserIdValidationFilter invoked for path: " + path); // Log cookies for debugging @@ -73,8 +133,7 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } // Log headers for debugging - String jwtTokenFromHeader = request.getHeader("Jwttoken"); - logger.info("JWT token from header: "); + logger.debug("JWT token from header: {}", request.getHeader("Jwttoken") != null ? "present" : "not present"); // Skip authentication for public endpoints if (shouldSkipAuthentication(path, contextPath)) { @@ -146,6 +205,36 @@ private boolean isOriginAllowed(String origin) { }); } + /** + * Get allowed HTTP methods for a given endpoint path. + * Checks against ENDPOINT_ALLOWED_METHODS map with wildcard support. + * Returns DEFAULT_ALLOWED_METHODS if endpoint is not configured. + * + * @param endpointPath The endpoint path (relative, without context path) + * @return Set of allowed HTTP methods for this endpoint + */ + private Set getAllowedMethodsForEndpoint(String endpointPath) { + // Check exact match first + if (ENDPOINT_ALLOWED_METHODS.containsKey(endpointPath)) { + return ENDPOINT_ALLOWED_METHODS.get(endpointPath); + } + + // Check wildcard patterns (e.g., /dynamicForm/delete/*/field) + for (Map.Entry> entry : ENDPOINT_ALLOWED_METHODS.entrySet()) { + String pattern = entry.getKey(); + // Convert wildcard pattern to regex: escape special chars, then replace * with [^/]+ + String regex = pattern + .replace(".", "\\.") + .replace("*", "[^/]+"); // * matches one or more non-slash characters + if (endpointPath.matches(regex)) { + return entry.getValue(); + } + } + + // Default: only GET, POST, OPTIONS allowed + return DEFAULT_ALLOWED_METHODS; + } + private boolean isMobileClient(String userAgent) { if (userAgent == null) return false; diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index 1c322dc4..375f02a8 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -22,11 +22,13 @@ package com.iemr.common.utils.http; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import javax.ws.rs.core.MediaType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -45,6 +47,9 @@ public class HTTPRequestInterceptor implements HandlerInterceptor { Logger logger = LoggerFactory.getLogger(this.getClass().getSimpleName()); + @Value("${cors.allowed-origins}") + private String allowedOrigins; + @Autowired public void setValidator(Validator validator) { this.validator = validator; @@ -140,7 +145,14 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 response.setContentType(MediaType.APPLICATION_JSON); - response.setHeader("Access-Control-Allow-Origin", "*"); + + String origin = request.getHeader("Origin"); + if (origin != null && isOriginAllowed(origin)) { + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Credentials", "true"); + } else if (origin != null) { + logger.warn("CORS headers NOT added for error response | Unauthorized origin: {}", origin); + } // Better to use getBytes().length for accurate byte size byte[] responseBytes = jsonErrorResponse.getBytes(StandardCharsets.UTF_8); @@ -182,4 +194,27 @@ public void afterCompletion(HttpServletRequest request, HttpServletResponse resp throws Exception { logger.debug("In afterCompletion Request Completed"); } + + /** + * Check if the given origin is allowed based on configured allowedOrigins. + * Uses the same logic as JwtUserIdValidationFilter for consistency. + * + * @param origin The origin to validate + * @return true if origin is allowed, false otherwise + */ + private boolean isOriginAllowed(String origin) { + if (origin == null || allowedOrigins == null || allowedOrigins.trim().isEmpty()) { + return false; + } + + return Arrays.stream(allowedOrigins.split(",")) + .map(String::trim) + .anyMatch(pattern -> { + String regex = pattern + .replace(".", "\\.") + .replace("*", ".*") + .replace("http://localhost:.*", "http://localhost:\\d+"); // special case for wildcard port + return origin.matches(regex); + }); + } } From 4081825fdac5fe81c3fdc54e4500c8b743781d0f Mon Sep 17 00:00:00 2001 From: 5Amogh Date: Thu, 13 Nov 2025 10:13:27 +0530 Subject: [PATCH 2/4] fix: amm 1927 implementing suggested code fixes by coderabbit --- src/main/java/com/iemr/common/config/CorsConfig.java | 3 ++- .../com/iemr/common/utils/JwtUserIdValidationFilter.java | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/iemr/common/config/CorsConfig.java b/src/main/java/com/iemr/common/config/CorsConfig.java index f711eb49..fea2cc04 100644 --- a/src/main/java/com/iemr/common/config/CorsConfig.java +++ b/src/main/java/com/iemr/common/config/CorsConfig.java @@ -33,7 +33,8 @@ public void addCorsMappings(CorsRegistry registry) { .map(String::trim) .toArray(String[]::new)) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("Authorization", "Content-Type", "Accept", "Jwttoken") + .allowedHeaders("Authorization", "Content-Type", "Accept", "Jwttoken", + "serverAuthorization", "ServerAuthorization", "serverauthorization", "Serverauthorization") .exposedHeaders("Authorization", "Jwttoken") .allowCredentials(true) .maxAge(3600); diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index f708c633..8a8cb453 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -3,7 +3,6 @@ import java.io.IOException; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -35,11 +34,8 @@ public class JwtUserIdValidationFilter implements Filter { private static final Map> ENDPOINT_ALLOWED_METHODS = new HashMap<>(); static { - Set dynamicFormMethods = new HashSet<>(); - dynamicFormMethods.add("GET"); - dynamicFormMethods.add("POST"); - dynamicFormMethods.add("DELETE"); - ENDPOINT_ALLOWED_METHODS.put("/dynamicForm/delete/*/field", dynamicFormMethods); + ENDPOINT_ALLOWED_METHODS.put("/dynamicForm/delete/*/field", + Set.of("GET", "POST", "DELETE")); } public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, From 6112133c36ba9b6a20bc10a69b64248b1a9c07e6 Mon Sep 17 00:00:00 2001 From: 5Amogh Date: Thu, 13 Nov 2025 10:46:17 +0530 Subject: [PATCH 3/4] fix: amm 1927 removal of hardcoded localhost part --- .../java/com/iemr/common/utils/JwtUserIdValidationFilter.java | 3 +-- .../com/iemr/common/utils/http/HTTPRequestInterceptor.java | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 8a8cb453..71688e4d 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -193,8 +193,7 @@ private boolean isOriginAllowed(String origin) { .anyMatch(pattern -> { String regex = pattern .replace(".", "\\.") - .replace("*", ".*") - .replace("http://localhost:.*", "http://localhost:\\d+"); // special case for wildcard port + .replace("*", ".*"); boolean matched = origin.matches(regex); return matched; diff --git a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java index 375f02a8..0c609839 100644 --- a/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java +++ b/src/main/java/com/iemr/common/utils/http/HTTPRequestInterceptor.java @@ -212,8 +212,7 @@ private boolean isOriginAllowed(String origin) { .anyMatch(pattern -> { String regex = pattern .replace(".", "\\.") - .replace("*", ".*") - .replace("http://localhost:.*", "http://localhost:\\d+"); // special case for wildcard port + .replace("*", ".*"); return origin.matches(regex); }); } From 106515c424606b6086b212560fe61dfd3bd983c6 Mon Sep 17 00:00:00 2001 From: 5Amogh Date: Thu, 13 Nov 2025 12:37:56 +0530 Subject: [PATCH 4/4] fix: amm 1927 removal of restrictive pattern for options & sticking REST API standards --- .../com/iemr/common/config/CorsConfig.java | 20 +------ .../utils/JwtUserIdValidationFilter.java | 60 +++---------------- 2 files changed, 11 insertions(+), 69 deletions(-) diff --git a/src/main/java/com/iemr/common/config/CorsConfig.java b/src/main/java/com/iemr/common/config/CorsConfig.java index fea2cc04..fdd9b494 100644 --- a/src/main/java/com/iemr/common/config/CorsConfig.java +++ b/src/main/java/com/iemr/common/config/CorsConfig.java @@ -11,20 +11,6 @@ public class CorsConfig implements WebMvcConfigurer { @Value("${cors.allowed-origins}") private String allowedOrigins; - - /** - * Spring MVC CORS configuration (framework level). - * - * NOTE: This configuration is permissive at the Spring framework level. - * Actual granular CORS enforcement (origin validation, endpoint-specific method control) - * is handled by JwtUserIdValidationFilter, which implements a two-layer security approach: - * - * 1. Spring CORS config: Permissive at framework level (allows PUT/DELETE for all endpoints) - * 2. JwtUserIdValidationFilter: Enforces strict origin validation and endpoint-specific method restrictions - * - * This design allows Spring to handle CORS preflight requests, while the filter enforces - * security policies before requests reach controllers. - */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -32,9 +18,9 @@ public void addCorsMappings(CorsRegistry registry) { Arrays.stream(allowedOrigins.split(",")) .map(String::trim) .toArray(String[]::new)) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("Authorization", "Content-Type", "Accept", "Jwttoken", - "serverAuthorization", "ServerAuthorization", "serverauthorization", "Serverauthorization") + .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS") + .allowedHeaders("Authorization", "Content-Type", "Accept", "Jwttoken", + "serverAuthorization", "ServerAuthorization", "serverauthorization", "Serverauthorization") .exposedHeaders("Authorization", "Jwttoken") .allowCredentials(true) .maxAge(3600); diff --git a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java index 71688e4d..412352fc 100644 --- a/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java +++ b/src/main/java/com/iemr/common/utils/JwtUserIdValidationFilter.java @@ -26,17 +26,6 @@ public class JwtUserIdValidationFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); private final String allowedOrigins; - // Default allowed methods for unconfigured endpoints - private static final Set DEFAULT_ALLOWED_METHODS = Set.of("GET", "POST", "OPTIONS"); - - // Endpoint-specific method control map - // Key = endpoint path pattern (supports wildcards), Value = Set of allowed HTTP methods - private static final Map> ENDPOINT_ALLOWED_METHODS = new HashMap<>(); - - static { - ENDPOINT_ALLOWED_METHODS.put("/dynamicForm/delete/*/field", - Set.of("GET", "POST", "DELETE")); - } public JwtUserIdValidationFilter(JwtAuthenticationUtil jwtAuthenticationUtil, String allowedOrigins) { @@ -81,24 +70,14 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } } - // STEP 2: Endpoint-Specific Method Validation + // Determine request path/context for later checks String path = request.getRequestURI(); String contextPath = request.getContextPath(); - String relativePath = path.startsWith(contextPath) ? path.substring(contextPath.length()) : path; - - Set allowedMethods = getAllowedMethodsForEndpoint(relativePath); - if (!allowedMethods.contains(method.toUpperCase())) { - logger.warn("BLOCKED - Method Not Allowed | Method: {} | URI: {} | Allowed Methods: {}", - method, uri, allowedMethods); - response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, - "Method " + method + " not allowed for this endpoint"); - return; - } // STEP 3: Add CORS Headers (only for validated origins) if (origin != null && isOriginAllowed(origin)) { response.setHeader("Access-Control-Allow-Origin", origin); // Never use wildcard - response.setHeader("Access-Control-Allow-Methods", String.join(", ", allowedMethods) + ", OPTIONS"); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Jwttoken, serverAuthorization, ServerAuthorization, serverauthorization, Serverauthorization"); response.setHeader("Access-Control-Allow-Credentials", "true"); @@ -108,7 +87,12 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo // STEP 4: Handle OPTIONS Preflight Request if ("OPTIONS".equalsIgnoreCase(method)) { - logger.info("OPTIONS preflight request - skipping JWT validation"); + // OPTIONS (preflight) - respond with full allowed methods + response.setHeader("Access-Control-Allow-Origin", origin); + response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", + "Authorization, Content-Type, Accept, Jwttoken, serverAuthorization, ServerAuthorization, serverauthorization, Serverauthorization"); + response.setHeader("Access-Control-Allow-Credentials", "true"); response.setStatus(HttpServletResponse.SC_OK); return; } @@ -200,35 +184,7 @@ private boolean isOriginAllowed(String origin) { }); } - /** - * Get allowed HTTP methods for a given endpoint path. - * Checks against ENDPOINT_ALLOWED_METHODS map with wildcard support. - * Returns DEFAULT_ALLOWED_METHODS if endpoint is not configured. - * - * @param endpointPath The endpoint path (relative, without context path) - * @return Set of allowed HTTP methods for this endpoint - */ - private Set getAllowedMethodsForEndpoint(String endpointPath) { - // Check exact match first - if (ENDPOINT_ALLOWED_METHODS.containsKey(endpointPath)) { - return ENDPOINT_ALLOWED_METHODS.get(endpointPath); - } - - // Check wildcard patterns (e.g., /dynamicForm/delete/*/field) - for (Map.Entry> entry : ENDPOINT_ALLOWED_METHODS.entrySet()) { - String pattern = entry.getKey(); - // Convert wildcard pattern to regex: escape special chars, then replace * with [^/]+ - String regex = pattern - .replace(".", "\\.") - .replace("*", "[^/]+"); // * matches one or more non-slash characters - if (endpointPath.matches(regex)) { - return entry.getValue(); - } - } - // Default: only GET, POST, OPTIONS allowed - return DEFAULT_ALLOWED_METHODS; - } private boolean isMobileClient(String userAgent) { if (userAgent == null)