Cors Wildcard in Spring Boot with Hmac Signatures
Cors Wildcard in Spring Boot with Hmac Signatures — how this specific combination creates or exposes the vulnerability
A CORS wildcard (allowedOrigins = "*") combined with Hmac Signatures in Spring Boot can unintentionally expose authenticated or integrity-checked endpoints to any origin. When Hmac Signatures are used, the client typically includes a signature header derived from a shared secret and request attributes (e.g., timestamp, nonce, payload). If the server responds with Access-Control-Allow-Origin: * to a preflight or actual request that carries credentials or sensitive headers, any website can make requests on behalf of or observe responses intended for authenticated clients.
In Spring Boot, CORS configuration is often set globally or per path. A common insecure pattern is:
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("X-Request-ID");
}
};
}
With Hmac Signatures, the server validates a signature header (e.g., X-API-Signature) before processing the request. Even if the signature is valid, a wildcard CORS policy means any origin can trigger these validated requests. This becomes critical when the endpoint performs actions on behalf of the user (e.g., changing settings) or returns sensitive data. An attacker site can embed a form or script that issues signed requests using the victim’s credentials (cookies or tokens), leading to unauthorized operations or information leakage through the response if credentials are included via cookies or authorization headers.
The interaction with preflight requests also matters. For non-simple requests (e.g., custom headers like X-API-Signature), the browser sends an OPTIONS preflight. If the server responds with Access-Control-Allow-Origin: * and does not reflect the requesting origin, the browser may block the actual request in secure contexts, but misconfigurations can allow the request to proceed. More importantly, wildcard CORS with exposed sensitive headers undermines the boundary that Hmac Signatures aim to enforce: that only known origins can craft valid interactions.
To avoid this, origins should be explicitly listed or dynamically set based on an allowlist, rather than using a wildcard, especially when Hmac Signatures protect state-changing or sensitive endpoints. This ensures that only trusted origins can initiate requests that will be validated with the shared secret.
Hmac Signatures-Specific Remediation in Spring Boot — concrete code fixes
Remediation centers on two changes: tighten CORS to avoid wildcards and ensure Hmac Signature validation is applied consistently before any business logic. Below are concrete, secure configurations and a Hmac signature validation filter for Spring Boot.
Secure CORS Configuration
Replace the wildcard with an explicit origin or a controlled allowlist. If you need dynamic origins, compute the allowed list at runtime and set the response header explicitly; do not use allowedOrigins("*") when credentials or sensitive headers are involved.
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://app.yourcompany.com", "https://admin.yourcompany.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("X-API-Signature", "Content-Type", "Authorization")
.exposedHeaders("X-Request-ID")
.allowCredentials(true);
}
};
}
Hmac Signature Validation Filter
Implement a filter that computes the expected Hmac signature and compares it securely (constant-time comparison) before the request proceeds. This example uses SHA-256 with a shared secret stored as an environment variable.
@Component
public class HmacSignatureFilter extends OncePerRequestFilter {
private final String secret = System.getenv("API_HMAC_SECRET");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String signatureHeader = request.getHeader("X-API-Signature");
String timestamp = request.getHeader("X-API-Timestamp");
String nonce = request.getHeader("X-API-Nonce");
if (signatureHeader == null || timestamp == null || nonce == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Missing signature or headers");
return;
}
// Prevent replay attacks: ensure timestamp is recent (e.g., within 5 minutes)
long requestTime = Long.parseLong(timestamp);
long now = System.currentTimeMillis();
if (Math.abs(now - requestTime) > 5 * 60 * 1000) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Request expired");
return;
}
// Build the data to sign: method + path + timestamp + nonce + body
String method = request.getMethod();
String path = request.getRequestURI();
String body = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8);
String dataToSign = method + path + timestamp + nonce + body;
String computedSignature = calculateHmac(dataToSign, secret);
if (!constantTimeEquals(computedSignature, signatureHeader)) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid signature");
return;
}
filterChain.doFilter(request, response);
}
private String calculateHmac(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(keySpec);
return Hex.encodeHexString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Hmac calculation failed", e);
}
}
private boolean constantTimeEquals(String a, String b) {
if (a.length() != b.length()) {
return false;
}
int result = 0;
for (int i = 0; i < a.length(); i++) {
result |= a.charAt(i) ^ b.charAt(i);
}
return result == 0;
}
}
Register the filter in your security configuration to ensure it runs before authentication/authorization:
@Configuration
@WebFilter(urlPatterns = "/api/*")
public class FilterConfig {
@Bean
public FilterRegistrationBean hmacFilter() {
FilterRegistrationBean registration = new FilterRegistrationBean<>();
registration.setFilter(new HmacSignatureFilter());
registration.setOrder(1);
return registration;
}
}
Additional guidance:
- Use HTTPS to prevent interception of the signature, timestamp, and nonce.
- Store the shared secret securely (e.g., via a secrets manager) and rotate periodically.
- Include a nonce and short timestamp window to mitigate replay attacks.
- Avoid logging the signature or secret in production logs.
Related CWEs: dataExposure
| CWE ID | Name | Severity |
|---|---|---|
| CWE-200 | Exposure of Sensitive Information | HIGH |
| CWE-209 | Error Information Disclosure | MEDIUM |
| CWE-213 | Exposure of Sensitive Information Due to Incompatible Policies | HIGH |
| CWE-215 | Insertion of Sensitive Information Into Debugging Code | MEDIUM |
| CWE-312 | Cleartext Storage of Sensitive Information | HIGH |
| CWE-359 | Exposure of Private Personal Information (PII) | HIGH |
| CWE-522 | Insufficiently Protected Credentials | CRITICAL |
| CWE-532 | Insertion of Sensitive Information into Log File | MEDIUM |
| CWE-538 | Insertion of Sensitive Information into Externally-Accessible File | HIGH |
| CWE-540 | Inclusion of Sensitive Information in Source Code | HIGH |