Information Disclosure via Permissive CORS Configuration

Medium Risk information-disclosure
javacorsweb-securityinformation-disclosuresame-origin-policy

What it is

Information disclosure vulnerabilities occur when Cross-Origin Resource Sharing (CORS) is configured to allow any origin (*) or null origin to access resources, potentially exposing sensitive data to malicious websites.

@RestControllerpublic class ApiController {        @GetMapping("/api/user-data")    public ResponseEntity<UserData> getUserData(HttpServletRequest request, HttpServletResponse response) {        // VULNERABLE: Wildcard allows any origin        response.setHeader("Access-Control-Allow-Origin", "*");        response.setHeader("Access-Control-Allow-Credentials", "true");                UserData userData = userService.getCurrentUserData();        return ResponseEntity.ok(userData);    }        @PostMapping("/api/sensitive-action")    public ResponseEntity<String> performAction(HttpServletResponse response) {        // VULNERABLE: Allowing null origin        String origin = request.getHeader("Origin");        if (origin == null || origin.equals("null")) {            response.setHeader("Access-Control-Allow-Origin", "null");        } else {            response.setHeader("Access-Control-Allow-Origin", origin);        }        response.setHeader("Access-Control-Allow-Credentials", "true");                // Perform sensitive action...        return ResponseEntity.ok("Action completed");    }}@Componentpublic class CorsFilter implements Filter {        @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)             throws IOException, ServletException {                HttpServletResponse httpResponse = (HttpServletResponse) response;                // VULNERABLE: Global wildcard CORS        httpResponse.setHeader("Access-Control-Allow-Origin", "*");        httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");        httpResponse.setHeader("Access-Control-Allow-Headers", "*");        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");                chain.doFilter(request, response);    }}@Configuration@EnableWebSecuritypublic class SecurityConfig {        @Bean    public CorsConfigurationSource corsConfigurationSource() {        CorsConfiguration configuration = new CorsConfiguration();                // VULNERABLE: Allowing all origins        configuration.setAllowedOriginPatterns(Arrays.asList("*"));        configuration.setAllowedMethods(Arrays.asList("*"));        configuration.setAllowedHeaders(Arrays.asList("*"));        configuration.setAllowCredentials(true);                UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();        source.registerCorsConfiguration("/**", configuration);        return source;    }}
@RestControllerpublic class ApiController {        private static final Set<String> ALLOWED_ORIGINS = Set.of(        "https://myapp.com",        "https://www.myapp.com",        "https://admin.myapp.com"    );        @GetMapping("/api/user-data")    public ResponseEntity<UserData> getUserData(HttpServletRequest request, HttpServletResponse response) {        String origin = request.getHeader("Origin");                // SECURE: Validate origin against allowlist        if (origin != null && ALLOWED_ORIGINS.contains(origin)) {            response.setHeader("Access-Control-Allow-Origin", origin);            response.setHeader("Access-Control-Allow-Credentials", "true");            response.setHeader("Vary", "Origin");        }                UserData userData = userService.getCurrentUserData();        return ResponseEntity.ok(userData);    }        @PostMapping("/api/sensitive-action")    public ResponseEntity<String> performAction(HttpServletRequest request, HttpServletResponse response) {        String origin = request.getHeader("Origin");                // SECURE: Strict origin validation        if (origin == null || !ALLOWED_ORIGINS.contains(origin)) {            return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Origin not allowed");        }                response.setHeader("Access-Control-Allow-Origin", origin);        response.setHeader("Access-Control-Allow-Credentials", "true");        response.setHeader("Vary", "Origin");                // Perform sensitive action...        return ResponseEntity.ok("Action completed");    }}@Componentpublic class SecureCorsFilter implements Filter {        private static final Set<String> ALLOWED_ORIGINS = Set.of(        "https://myapp.com",        "https://www.myapp.com",        "https://api.myapp.com"    );        private static final Set<String> ALLOWED_METHODS = Set.of(        "GET", "POST", "PUT", "DELETE", "OPTIONS"    );        private static final Set<String> ALLOWED_HEADERS = Set.of(        "Content-Type", "Authorization", "X-Requested-With"    );        @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)             throws IOException, ServletException {                HttpServletRequest httpRequest = (HttpServletRequest) request;        HttpServletResponse httpResponse = (HttpServletResponse) response;                String origin = httpRequest.getHeader("Origin");        String method = httpRequest.getMethod();                // SECURE: Validate origin against allowlist        if (origin != null && ALLOWED_ORIGINS.contains(origin)) {            httpResponse.setHeader("Access-Control-Allow-Origin", origin);            httpResponse.setHeader("Vary", "Origin");                        // Handle preflight requests            if ("OPTIONS".equals(method)) {                String requestMethod = httpRequest.getHeader("Access-Control-Request-Method");                String requestHeaders = httpRequest.getHeader("Access-Control-Request-Headers");                                if (requestMethod != null && ALLOWED_METHODS.contains(requestMethod)) {                    httpResponse.setHeader("Access-Control-Allow-Methods", requestMethod);                }                                if (requestHeaders != null) {                    String[] headers = requestHeaders.split(",");                    List<String> allowedRequestHeaders = Arrays.stream(headers)                        .map(String::trim)                        .filter(ALLOWED_HEADERS::contains)                        .collect(Collectors.toList());                                        if (!allowedRequestHeaders.isEmpty()) {                        httpResponse.setHeader("Access-Control-Allow-Headers",                             String.join(", ", allowedRequestHeaders));                    }                }                                httpResponse.setHeader("Access-Control-Max-Age", "3600");                httpResponse.setStatus(HttpServletResponse.SC_OK);                return;            }                        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");        }                chain.doFilter(request, response);    }}@Configuration@EnableWebSecuritypublic class SecurityConfig {        @Bean    public CorsConfigurationSource corsConfigurationSource() {        CorsConfiguration configuration = new CorsConfiguration();                // SECURE: Specific allowed origins only        configuration.setAllowedOrigins(Arrays.asList(            "https://myapp.com",            "https://www.myapp.com",            "https://admin.myapp.com"        ));                configuration.setAllowedMethods(Arrays.asList(            "GET", "POST", "PUT", "DELETE", "OPTIONS"        ));                configuration.setAllowedHeaders(Arrays.asList(            "Content-Type", "Authorization", "X-Requested-With"        ));                configuration.setAllowCredentials(true);        configuration.setMaxAge(3600L);                UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();        source.registerCorsConfiguration("/**", configuration);        return source;    }        @Bean    public WebMvcConfigurer corsConfigurer() {        return new WebMvcConfigurer() {            @Override            public void addCorsMappings(CorsRegistry registry) {                registry.addMapping("/api/**")                    .allowedOrigins(                        "https://myapp.com",                        "https://www.myapp.com"                    )                    .allowedMethods("GET", "POST", "PUT", "DELETE")                    .allowedHeaders("Content-Type", "Authorization")                    .allowCredentials(true)                    .maxAge(3600);            }        };    }}

💡 Why This Fix Works

The vulnerable code was updated to address the security issue.

Why it happens

Setting Access-Control-Allow-Origin: * to allow any origin, exposing APIs to cross-origin attacks.

Root causes

Wildcard CORS Origins

Setting Access-Control-Allow-Origin: * to allow any origin, exposing APIs to cross-origin attacks.

Reflecting Request Origin

Dynamically setting CORS headers to match the request Origin header without validation.

Development Configuration in Production

Leaving permissive CORS settings from local development active in production deployments.

Fixes

1

Whitelist Specific Origins

Replace wildcard (*) with an explicit list of trusted origins that should have cross-origin access.

2

Validate Origin Headers

Check the request Origin header against an allowlist before setting Access-Control-Allow-Origin in responses.

3

Use Credentials Carefully

Never combine Access-Control-Allow-Credentials: true with Allow-Origin: *, as this is forbidden and insecure.

Detect This Vulnerability in Your Code

Sourcery automatically identifies information disclosure via permissive cors configuration and many other security issues in your codebase.