No CORS configuration — will block frontend or risk misconfiguration #10

Open
opened 2026-04-02 11:21:10 +02:00 by marcel · 5 comments
Owner

Problem

SecurityConfig has no .cors() configuration. Spring Security defaults to no CORS headers, which will block any SvelteKit frontend on a different origin. When CORS is eventually added, there's a risk of overly permissive configuration (e.g., allowedOrigins("*") with credentials).

Affected files

  • SecurityConfig.java — no .cors() block

Add explicit CORS configuration that:

  • Specifies exact allowed origins (not *)
  • Only allows necessary HTTP methods
  • Does not allow credentials with wildcard origins
  • Sets appropriate maxAge for preflight caching

Severity

Medium — currently blocks legitimate frontend access; risk of insecure configuration when added.

## Problem `SecurityConfig` has no `.cors()` configuration. Spring Security defaults to no CORS headers, which will block any SvelteKit frontend on a different origin. When CORS is eventually added, there's a risk of overly permissive configuration (e.g., `allowedOrigins("*")` with credentials). ## Affected files - `SecurityConfig.java` — no `.cors()` block ## Recommended fix Add explicit CORS configuration that: - Specifies exact allowed origins (not `*`) - Only allows necessary HTTP methods - Does not allow credentials with wildcard origins - Sets appropriate `maxAge` for preflight caching ## Severity Medium — currently blocks legitimate frontend access; risk of insecure configuration when added.
marcel added the kind/securitypriority/medium labels 2026-04-02 11:21:45 +02:00
Author
Owner

👨‍💻 Kai — Frontend Engineer

CORS is directly my problem — without it, my SvelteKit frontend can't talk to the backend at all. This is a blocker for any local dev work once I have the frontend running on a different port.

What I need from the CORS config

  • http://localhost:5173 (Vite dev server default) must be in the allowed origins during development.
  • The production origin (whatever the deployed frontend URL is) must be in the allowed origins for production.
  • credentials: true must be set on the CORS config — we use session cookies, and fetch requests with credentials: 'include' won't work without it. This is non-negotiable for session auth.
  • PATCH must be in the allowed methods if we use it anywhere (SvelteKit form actions can use POST with method override, but direct API calls may use PATCH).

My fetch setup

  • All my server-side fetch calls in +page.server.ts go through the SvelteKit backend (Node.js) which is same-origin with my server — so CORS headers are needed only for browser-initiated requests. Server-to-server calls in load functions won't be blocked by CORS.
  • However, any future client-side fetch (e.g., real-time shopping list updates in D1) will be browser-initiated and will need CORS to be correct.

Questions

  • What's the production deployment topology? Same-origin (frontend and backend behind the same reverse proxy)? Or separate domains? The answer determines whether CORS is needed in production at all — if they share a domain, CORS is a dev-only concern.
  • Will we use environment-specific CORS config (dev allows localhost:5173, prod allows only the production domain)? That's the right approach — can the backend read allowed origins from application.yml profiles?
## 👨‍💻 Kai — Frontend Engineer CORS is directly my problem — without it, my SvelteKit frontend can't talk to the backend at all. This is a blocker for any local dev work once I have the frontend running on a different port. **What I need from the CORS config** - `http://localhost:5173` (Vite dev server default) must be in the allowed origins during development. - The production origin (whatever the deployed frontend URL is) must be in the allowed origins for production. - `credentials: true` must be set on the CORS config — we use session cookies, and `fetch` requests with `credentials: 'include'` won't work without it. This is non-negotiable for session auth. - `PATCH` must be in the allowed methods if we use it anywhere (SvelteKit form actions can use `POST` with method override, but direct API calls may use `PATCH`). **My fetch setup** - All my server-side fetch calls in `+page.server.ts` go through the SvelteKit backend (Node.js) which is same-origin with my server — so CORS headers are needed only for browser-initiated requests. Server-to-server calls in load functions won't be blocked by CORS. - However, any future client-side fetch (e.g., real-time shopping list updates in D1) will be browser-initiated and will need CORS to be correct. **Questions** - What's the production deployment topology? Same-origin (frontend and backend behind the same reverse proxy)? Or separate domains? The answer determines whether CORS is needed in production at all — if they share a domain, CORS is a dev-only concern. - Will we use environment-specific CORS config (dev allows `localhost:5173`, prod allows only the production domain)? That's the right approach — can the backend read allowed origins from `application.yml` profiles?
Author
Owner

🛠️ Backend Engineer

CORS configuration is one of those things that's trivial to get wrong and painful to debug. Let's get it right the first time.

Recommended implementation

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(List.of("http://localhost:5173")); // dev; prod via profile
    config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
    config.setAllowedHeaders(List.of("Content-Type", "X-CSRF-Token")); // add as needed
    config.setAllowCredentials(true); // required for session cookies
    config.setMaxAge(3600L); // cache preflight for 1 hour
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);
    return source;
}

Then in SecurityFilterChain:

http.cors(cors -> cors.configurationSource(corsConfigurationSource()))

Critical: allowCredentials(true) cannot be combined with allowedOrigins("*")

Spring Security (and the browser) will reject this combination. You must specify exact origins. If the list needs to be dynamic (e.g., multiple dev machines), use setAllowedOriginPatterns() instead of setAllowedOrigins() — patterns support wildcards while still being compatible with allowCredentials(true).

Profile-based configuration

Allowed origins should come from application.yml:

# application-dev.yml
cors.allowed-origins: http://localhost:5173

# application-prod.yml
cors.allowed-origins: https://mealprep.example.com

Then inject via @Value("${cors.allowed-origins}") or a @ConfigurationProperties class.

Questions

  • Are we planning to run frontend and backend on different domains in production, or behind a single reverse proxy (same origin)? If same origin in prod, CORS is only needed for local development.
  • Do we have a @ControllerAdvice that handles OPTIONS preflight requests, or will Spring Security's CORS filter handle that? The http.cors() configuration handles it automatically — just want to confirm we're not double-handling it.
## 🛠️ Backend Engineer CORS configuration is one of those things that's trivial to get wrong and painful to debug. Let's get it right the first time. **Recommended implementation** ```java @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("http://localhost:5173")); // dev; prod via profile config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); config.setAllowedHeaders(List.of("Content-Type", "X-CSRF-Token")); // add as needed config.setAllowCredentials(true); // required for session cookies config.setMaxAge(3600L); // cache preflight for 1 hour UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } ``` Then in `SecurityFilterChain`: ```java http.cors(cors -> cors.configurationSource(corsConfigurationSource())) ``` **Critical: `allowCredentials(true)` cannot be combined with `allowedOrigins("*")`** Spring Security (and the browser) will reject this combination. You must specify exact origins. If the list needs to be dynamic (e.g., multiple dev machines), use `setAllowedOriginPatterns()` instead of `setAllowedOrigins()` — patterns support wildcards while still being compatible with `allowCredentials(true)`. **Profile-based configuration** Allowed origins should come from `application.yml`: ```yaml # application-dev.yml cors.allowed-origins: http://localhost:5173 # application-prod.yml cors.allowed-origins: https://mealprep.example.com ``` Then inject via `@Value("${cors.allowed-origins}")` or a `@ConfigurationProperties` class. **Questions** - Are we planning to run frontend and backend on different domains in production, or behind a single reverse proxy (same origin)? If same origin in prod, CORS is only needed for local development. - Do we have a `@ControllerAdvice` that handles `OPTIONS` preflight requests, or will Spring Security's CORS filter handle that? The `http.cors()` configuration handles it automatically — just want to confirm we're not double-handling it.
Author
Owner

🧪 QA Engineer

CORS is one of those areas where the tests need to verify the security properties, not just "does it work" — because a permissive misconfiguration passes functional tests but fails security tests.

Tests I'd add

Preflight request tests:

  • shouldRespondToPreflightWithCorrectAllowedOrigin()OPTIONS /v1/recipes with Origin: http://localhost:5173 → assert 200, assert Access-Control-Allow-Origin: http://localhost:5173.
  • shouldNotAllowUnknownOriginInPreflight()OPTIONS /v1/recipes with Origin: http://evil.example.com → assert no Access-Control-Allow-Origin header in response (or assert the header is absent/null).
  • shouldIncludeAllowCredentialsTrueInPreflightResponse() — assert Access-Control-Allow-Credentials: true is present for allowed origins.

Actual cross-origin request tests:

  • shouldIncludeCorsHeadersOnGetRequest()GET /v1/recipes with Origin: http://localhost:5173 → assert Access-Control-Allow-Origin header present.
  • shouldNotIncludeCorsHeadersForUnknownOrigin()GET /v1/recipes with Origin: http://evil.example.com → assert no Access-Control-Allow-Origin header.

Misconfiguration guard test:

  • shouldNotAllowWildcardOriginWithCredentials() — a unit test or config test that verifies the CORS config bean does not have allowedOrigins("*") set when allowCredentials(true) is configured. This is a static assertion, but it's valuable as a regression guard.

Question

  • Are the CORS tests going to run as part of the integration test suite with @SpringBootTest? They need to spin up the full security filter chain to be meaningful — mocking CorsConfigurationSource in a unit test doesn't verify that http.cors() is actually wired up.
  • Do we have a dev profile active during tests? The allowed origins list is likely profile-dependent, so the test environment needs the right profile to match the expected origin in the assertions.
## 🧪 QA Engineer CORS is one of those areas where the tests need to verify the security properties, not just "does it work" — because a permissive misconfiguration passes functional tests but fails security tests. **Tests I'd add** Preflight request tests: - `shouldRespondToPreflightWithCorrectAllowedOrigin()` — `OPTIONS /v1/recipes` with `Origin: http://localhost:5173` → assert 200, assert `Access-Control-Allow-Origin: http://localhost:5173`. - `shouldNotAllowUnknownOriginInPreflight()` — `OPTIONS /v1/recipes` with `Origin: http://evil.example.com` → assert no `Access-Control-Allow-Origin` header in response (or assert the header is absent/null). - `shouldIncludeAllowCredentialsTrueInPreflightResponse()` — assert `Access-Control-Allow-Credentials: true` is present for allowed origins. Actual cross-origin request tests: - `shouldIncludeCorsHeadersOnGetRequest()` — `GET /v1/recipes` with `Origin: http://localhost:5173` → assert `Access-Control-Allow-Origin` header present. - `shouldNotIncludeCorsHeadersForUnknownOrigin()` — `GET /v1/recipes` with `Origin: http://evil.example.com` → assert no `Access-Control-Allow-Origin` header. Misconfiguration guard test: - `shouldNotAllowWildcardOriginWithCredentials()` — a unit test or config test that verifies the CORS config bean does not have `allowedOrigins("*")` set when `allowCredentials(true)` is configured. This is a static assertion, but it's valuable as a regression guard. **Question** - Are the CORS tests going to run as part of the integration test suite with `@SpringBootTest`? They need to spin up the full security filter chain to be meaningful — mocking `CorsConfigurationSource` in a unit test doesn't verify that `http.cors()` is actually wired up. - Do we have a dev profile active during tests? The allowed origins list is likely profile-dependent, so the test environment needs the right profile to match the expected origin in the assertions.
Author
Owner

🔒 Sable — Security Engineer

CORS misconfiguration is a consistent entry in the OWASP Top 10. The issue correctly identifies the two failure modes — let me add the threat model and the specific controls needed.

Threat: allowedOrigins("*") with allowCredentials(true)

This is the most dangerous CORS misconfiguration. It would allow any website to make credentialed requests to our API using the victim's session cookie. Attack scenario:

  1. User is logged in to mealprep.
  2. User visits evil.example.com.
  3. Evil page makes fetch("https://mealprep.example.com/v1/meals", { credentials: "include" }).
  4. Browser sends the request with the session cookie — bypassing same-origin policy because CORS explicitly permits it.
  5. Evil page reads the response.

Modern browsers and Spring Security both reject allowCredentials(true) + allowedOrigins("*") — but the risk is that a developer might use allowedOriginPatterns("*"), which does work with credentials and is effectively the same vulnerability.

Controls to enforce

  • Allowed origins must be an explicit, static list — or a validated list read from config. No wildcards.
  • allowCredentials(true) must be set (required for session cookies), but only with explicit origins.
  • allowedMethods should be the minimum required — not ["*"]. Enumerate the methods the API actually uses.
  • allowedHeaders should be explicit — not ["*"]. At minimum: Content-Type, X-CSRF-Token.
  • exposedHeaders should be empty unless we explicitly want the browser to read a response header (e.g., a pagination X-Total-Count header).

Questions before closing

  • What headers does our API return that the frontend needs to read? (e.g., Location on 201 Created, X-Total-Count for pagination) — those need to be in exposedHeaders.
  • Are there any third-party integrations (webhooks, OAuth callbacks) that need different CORS rules? If so, those paths need separate CorsConfiguration entries, not a blanket /** rule.
  • Is the CORS configuration covered by a security review checklist that runs before each release? CORS config changes are a common source of regressions.

My minimum bar: explicit origin list from config, no wildcards, allowCredentials(true), minimum method/header sets, integration test that verifies unknown origins are rejected.

## 🔒 Sable — Security Engineer CORS misconfiguration is a consistent entry in the OWASP Top 10. The issue correctly identifies the two failure modes — let me add the threat model and the specific controls needed. **Threat: `allowedOrigins("*")` with `allowCredentials(true)`** This is the most dangerous CORS misconfiguration. It would allow any website to make credentialed requests to our API using the victim's session cookie. Attack scenario: 1. User is logged in to mealprep. 2. User visits `evil.example.com`. 3. Evil page makes `fetch("https://mealprep.example.com/v1/meals", { credentials: "include" })`. 4. Browser sends the request with the session cookie — bypassing same-origin policy because CORS explicitly permits it. 5. Evil page reads the response. Modern browsers and Spring Security both reject `allowCredentials(true)` + `allowedOrigins("*")` — but the risk is that a developer might use `allowedOriginPatterns("*")`, which does work with credentials and is effectively the same vulnerability. **Controls to enforce** - Allowed origins must be an explicit, static list — or a validated list read from config. No wildcards. - `allowCredentials(true)` must be set (required for session cookies), but only with explicit origins. - `allowedMethods` should be the minimum required — not `["*"]`. Enumerate the methods the API actually uses. - `allowedHeaders` should be explicit — not `["*"]`. At minimum: `Content-Type`, `X-CSRF-Token`. - `exposedHeaders` should be empty unless we explicitly want the browser to read a response header (e.g., a pagination `X-Total-Count` header). **Questions before closing** - What headers does our API return that the frontend needs to read? (e.g., `Location` on 201 Created, `X-Total-Count` for pagination) — those need to be in `exposedHeaders`. - Are there any third-party integrations (webhooks, OAuth callbacks) that need different CORS rules? If so, those paths need separate `CorsConfiguration` entries, not a blanket `/**` rule. - Is the CORS configuration covered by a security review checklist that runs before each release? CORS config changes are a common source of regressions. **My minimum bar**: explicit origin list from config, no wildcards, `allowCredentials(true)`, minimum method/header sets, integration test that verifies unknown origins are rejected.
Author
Owner

🎨 Atlas — UI/UX Designer

This is infrastructure work with no direct visual output, but CORS directly affects the development experience and some loading states in the UI.

Developer experience impact

  • Without CORS, no browser-rendered component can make a credentialed request to the backend during local development. This blocks all UI development that requires real data — recipe lists, meal plans, shopping lists. Getting this right early means the team can work in parallel on frontend and backend without workarounds.

Loading state consideration

  • When CORS blocks a request, the browser doesn't give the app a useful error — it just fails with a network error. The UI shows a broken state with no meaningful message. This could be confused with a real data error during development.
  • Once CORS is properly configured, our loading and error states will be much easier to test — we'll see real API errors (401, 404, 500) instead of opaque network failures.

One UX question to settle alongside this

  • Are we planning a "maintenance mode" or "backend unavailable" state in the UI? A CORS failure looks identical to a backend-down failure from the frontend's perspective. If we ever need to distinguish them (e.g., for a more helpful error message), we'd need a health check endpoint that's CORS-accessible without session auth.
  • This is probably out of scope for v1, but worth knowing: does GET /actuator/health need to be in the allowed CORS paths? It might be useful for a simple "is the backend up?" check from the frontend.

No design blockers from my side — this is a prerequisite for development, not a UI feature. Ship it as soon as possible so Kai can start working with real data.

## 🎨 Atlas — UI/UX Designer This is infrastructure work with no direct visual output, but CORS directly affects the development experience and some loading states in the UI. **Developer experience impact** - Without CORS, no browser-rendered component can make a credentialed request to the backend during local development. This blocks all UI development that requires real data — recipe lists, meal plans, shopping lists. Getting this right early means the team can work in parallel on frontend and backend without workarounds. **Loading state consideration** - When CORS blocks a request, the browser doesn't give the app a useful error — it just fails with a network error. The UI shows a broken state with no meaningful message. This could be confused with a real data error during development. - Once CORS is properly configured, our loading and error states will be much easier to test — we'll see real API errors (401, 404, 500) instead of opaque network failures. **One UX question to settle alongside this** - Are we planning a "maintenance mode" or "backend unavailable" state in the UI? A CORS failure looks identical to a backend-down failure from the frontend's perspective. If we ever need to distinguish them (e.g., for a more helpful error message), we'd need a health check endpoint that's CORS-accessible without session auth. - This is probably out of scope for v1, but worth knowing: does `GET /actuator/health` need to be in the allowed CORS paths? It might be useful for a simple "is the backend up?" check from the frontend. **No design blockers** from my side — this is a prerequisite for development, not a UI feature. Ship it as soon as possible so Kai can start working with real data.
Sign in to join this conversation.