From b50b05967807784bfee4a0ce97266804c89b91e1 Mon Sep 17 00:00:00 2001 From: Joe Mahady Date: Wed, 29 Jul 2026 10:30:48 +0100 Subject: [PATCH 1/4] Implement AI Scan -- MEDIUM -- cors-allowed-origin-patterns-matched-with-find-instead-of-matches / uaa ai-assisted=yes Co-authored-by: Cursor --- .../identity/uaa/security/web/CorsFilter.java | 14 ++++++++++++-- .../identity/uaa/login/LoginMockMvcTests.java | 4 ++-- .../uaa/login/LoginMockMvcZonePathTests.java | 4 ++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java index 20d0a7246a4..c9df0e87c1e 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java @@ -324,7 +324,7 @@ protected boolean isAllowedRequestUri(final String uri, CorsConfiguration config protected boolean isAllowedOrigin(final String origin, CorsConfiguration configuration) { for (Pattern pattern : configuration.getAllowedOriginPatterns()) { // Making sure that the pattern matches - if (pattern.matcher(origin).find()) { + if (pattern.matcher(origin).matches()) { return true; } } @@ -366,6 +366,16 @@ private CorsConfiguration resolveDefaultCorsConfiguration() { return getDefaultConfiguration(); } + private String anchorPattern(String pattern) { + if (!pattern.startsWith("^")) { + pattern = "^" + pattern; + } + if (!pattern.endsWith("$")) { + pattern = pattern + "$"; + } + return pattern; + } + private void compileAllowedOriginsAndUris(CorsConfiguration configuration, String type) { if (configuration.getAllowedUris() != null) { for (String allowedUri : configuration.getAllowedUris()) { @@ -380,7 +390,7 @@ private void compileAllowedOriginsAndUris(CorsConfiguration configuration, Strin if (configuration.getAllowedOrigins() != null) { for (String allowedOrigin : configuration.getAllowedOrigins()) { try { - configuration.getAllowedOriginPatterns().add(Pattern.compile(allowedOrigin)); + configuration.getAllowedOriginPatterns().add(Pattern.compile(anchorPattern(allowedOrigin))); log.debug("Origin '%s' is allowed for a %s CORS requests.".formatted(allowedOrigin, type)); } catch (PatternSyntaxException patternSyntaxException) { log.error("Invalid regular expression pattern in cors.{}.allowed.origins: {}", type, allowedOrigin, patternSyntaxException); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index f7aa9797685..0bf704f8f45 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -2172,7 +2172,7 @@ void logOutCorsPreflight() throws Exception { */ @Test void logOutCorsPreflightForIdentityZone() throws Exception { - corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^*\\.localhost$")); + corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout.do$")); corsFilter.getFilter().initialize(); @@ -2266,7 +2266,7 @@ void logOutCorsPreflightWithUnallowedOrigin() throws Exception { @Test void xhrCorsPreflightForNonDefaultZoneWhenZoneSpecificCorsPolicyIsNull() throws Exception { // setting the default zone CORS policy - corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^*\\.localhost$")); + corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout.do$")); corsFilter.getFilter().initialize(); diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java index 33207b2e7ae..2ba3a7688a9 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java @@ -2622,7 +2622,7 @@ void logOutCorsPreflight(ZoneResolutionMode mode) throws Exception { void logOutCorsPreflightForIdentityZone(ZoneResolutionMode mode) throws Exception { String subdomain = "testzone1"; IdentityZone zone = MockMvcUtils.createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, false, IdentityZoneHolder.getCurrentZoneId()); - corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^*\\.localhost$")); + corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); List allowedUris = mode == ZoneResolutionMode.ZONE_PATH ? asList("^/logout.do$", "^/z/[^/]+/logout.do$") : singletonList("^/logout.do$"); corsFilter.getFilter().setCorsXhrAllowedUris(allowedUris); corsFilter.getFilter().initialize(); @@ -2734,7 +2734,7 @@ void logOutCorsPreflightWithUnallowedOrigin(ZoneResolutionMode mode) throws Exce @EnumSource(ZoneResolutionMode.class) void xhrCorsPreflightForNonDefaultZoneWhenZoneSpecificCorsPolicyIsNull(ZoneResolutionMode mode) throws Exception { // setting the default zone CORS policy - corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^*\\.localhost$")); + corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); // For ZONE_PATH mode, the request path is /z/{subdomain}/logout.do, so we need to allow that pattern List allowedUris = mode == ZoneResolutionMode.ZONE_PATH ? asList("^/logout.do$", "^/z/[^/]+/logout.do$") From c0c9ec0e72c3f17229ccfe5058476020812aa458 Mon Sep 17 00:00:00 2001 From: Joe Mahady Date: Fri, 7 Aug 2026 12:18:50 +0100 Subject: [PATCH 2/4] Fix tests failing after CorsFilter changes Co-authored-by: Cursor --- .../web/CorsFilterNonDefaultZoneTests.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/server/src/test/java/org/cloudfoundry/identity/uaa/security/web/CorsFilterNonDefaultZoneTests.java b/server/src/test/java/org/cloudfoundry/identity/uaa/security/web/CorsFilterNonDefaultZoneTests.java index be69c8ed7d5..4b232896fe2 100644 --- a/server/src/test/java/org/cloudfoundry/identity/uaa/security/web/CorsFilterNonDefaultZoneTests.java +++ b/server/src/test/java/org/cloudfoundry/identity/uaa/security/web/CorsFilterNonDefaultZoneTests.java @@ -92,8 +92,8 @@ void requestExpectXhrCorsResponse() throws Exception { @Test void requestWithAllowedOriginPatterns() throws Exception { - identityZone.getConfig().getCorsPolicy().getXhrConfiguration().getAllowedOriginPatterns() - .add(Pattern.compile("bunnyoutlet-shop.com$")); + identityZone.getConfig().getCorsPolicy().getXhrConfiguration().getAllowedOrigins() + .add("^.*bunnyoutlet-shop\\.com$"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uaa/userinfo"); request.addHeader("Origin", "bunnyoutlet-shop.com"); @@ -105,8 +105,8 @@ void requestWithAllowedOriginPatterns() throws Exception { @Test void requestWithAllowedUriPatterns() throws Exception { - identityZone.getConfig().getCorsPolicy().getXhrConfiguration().getAllowedUriPatterns() - .add(Pattern.compile("/uaa/*")); + identityZone.getConfig().getCorsPolicy().getXhrConfiguration().getAllowedUris() + .add("^/uaa/.*$"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uaa/login"); request.addHeader("Origin", "example.com"); @@ -258,8 +258,8 @@ void defaultCorsExpectStandardCorsResponse() throws Exception { @Test void defaultCorsWithAllowedOriginPatterns() throws Exception { - identityZone.getConfig().getCorsPolicy().getDefaultConfiguration().getAllowedOriginPatterns() - .add(Pattern.compile("bunnyoutlet.com$")); + identityZone.getConfig().getCorsPolicy().getDefaultConfiguration().getAllowedOrigins() + .add("^.*bunnyoutlet\\.com$"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uaa/userinfo"); request.addHeader("Origin", "bunnyoutlet.com"); @@ -270,8 +270,8 @@ void defaultCorsWithAllowedOriginPatterns() throws Exception { @Test void defaultCorsWithAllowedUriPatterns() throws Exception { - identityZone.getConfig().getCorsPolicy().getDefaultConfiguration().getAllowedUriPatterns() - .add(Pattern.compile("/uaa/*")); + identityZone.getConfig().getCorsPolicy().getDefaultConfiguration().getAllowedUris() + .add("^/uaa/.*$"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/uaa/login"); request.addHeader("Origin", "example.com"); From 508b647bd9e027c86313a5892b8bd2709d23a87b Mon Sep 17 00:00:00 2001 From: Joe Mahady Date: Fri, 7 Aug 2026 12:20:03 +0100 Subject: [PATCH 3/4] Fix cors allowed origin pattern matching --- docs/UAA-Configuration-Reference.md | 6 ++++++ .../cloudfoundry/identity/uaa/security/web/CorsFilter.java | 2 ++ .../cloudfoundry/identity/uaa/login/LoginMockMvcTests.java | 4 ++-- .../identity/uaa/login/LoginMockMvcZonePathTests.java | 6 +++--- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/UAA-Configuration-Reference.md b/docs/UAA-Configuration-Reference.md index 7fc4a0a401f..61a494b736c 100644 --- a/docs/UAA-Configuration-Reference.md +++ b/docs/UAA-Configuration-Reference.md @@ -2264,6 +2264,9 @@ Regex patterns for URIs that allow CORS requests (non-XHR). Default permits all. Regex patterns for allowed CORS origins (non-XHR). +**Note:** Origin patterns are evaluated as full-string matches (they are implicitly anchored with `^` and `$`). If you previously relied on substring matching, you must update your patterns. +For example, to safely match a domain with any subdomain and an optional port, use a pattern like: `^https?://([a-zA-Z0-9-]+\.)*example\.com(:[0-9]+)?$` + [Back to table](#cors) --- @@ -2336,6 +2339,9 @@ Regex patterns for URIs that allow XHR CORS requests. Regex patterns for allowed XHR CORS origins. +**Note:** Origin patterns are evaluated as full-string matches (they are implicitly anchored with `^` and `$`). If you previously relied on substring matching, you must update your patterns. +For example, to safely match a domain with any subdomain and an optional port, use a pattern like: `^https?://([a-zA-Z0-9-]+\.)*example\.com(:[0-9]+)?$` + [Back to table](#cors) --- diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java index c9df0e87c1e..d45ab6ff864 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java @@ -377,6 +377,7 @@ private String anchorPattern(String pattern) { } private void compileAllowedOriginsAndUris(CorsConfiguration configuration, String type) { + configuration.getAllowedUriPatterns().clear(); if (configuration.getAllowedUris() != null) { for (String allowedUri : configuration.getAllowedUris()) { try { @@ -387,6 +388,7 @@ private void compileAllowedOriginsAndUris(CorsConfiguration configuration, Strin } } } + configuration.getAllowedOriginPatterns().clear(); if (configuration.getAllowedOrigins() != null) { for (String allowedOrigin : configuration.getAllowedOrigins()) { try { diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java index 0bf704f8f45..4873bda27b8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcTests.java @@ -2173,7 +2173,7 @@ void logOutCorsPreflight() throws Exception { @Test void logOutCorsPreflightForIdentityZone() throws Exception { corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); - corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout.do$")); + corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout\\.do$")); corsFilter.getFilter().initialize(); HttpHeaders httpHeaders = new HttpHeaders(); @@ -2267,7 +2267,7 @@ void logOutCorsPreflightWithUnallowedOrigin() throws Exception { void xhrCorsPreflightForNonDefaultZoneWhenZoneSpecificCorsPolicyIsNull() throws Exception { // setting the default zone CORS policy corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); - corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout.do$")); + corsFilter.getFilter().setCorsXhrAllowedUris(singletonList("^/logout\\.do$")); corsFilter.getFilter().initialize(); // set the non default zone CORS Xhr policy to null diff --git a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java index 2ba3a7688a9..7ba12312ea8 100644 --- a/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java +++ b/uaa/src/test/java/org/cloudfoundry/identity/uaa/login/LoginMockMvcZonePathTests.java @@ -2623,7 +2623,7 @@ void logOutCorsPreflightForIdentityZone(ZoneResolutionMode mode) throws Exceptio String subdomain = "testzone1"; IdentityZone zone = MockMvcUtils.createOtherIdentityZone(subdomain, mockMvc, webApplicationContext, false, IdentityZoneHolder.getCurrentZoneId()); corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); - List allowedUris = mode == ZoneResolutionMode.ZONE_PATH ? asList("^/logout.do$", "^/z/[^/]+/logout.do$") : singletonList("^/logout.do$"); + List allowedUris = mode == ZoneResolutionMode.ZONE_PATH ? asList("^/logout\\.do$", "^/z/[^/]+/logout\\.do$") : singletonList("^/logout\\.do$"); corsFilter.getFilter().setCorsXhrAllowedUris(allowedUris); corsFilter.getFilter().initialize(); @@ -2737,8 +2737,8 @@ void xhrCorsPreflightForNonDefaultZoneWhenZoneSpecificCorsPolicyIsNull(ZoneResol corsFilter.getFilter().setCorsXhrAllowedOrigins(asList("^localhost$", "^.*\\.localhost$")); // For ZONE_PATH mode, the request path is /z/{subdomain}/logout.do, so we need to allow that pattern List allowedUris = mode == ZoneResolutionMode.ZONE_PATH - ? asList("^/logout.do$", "^/z/[^/]+/logout.do$") - : singletonList("^/logout.do$"); + ? asList("^/logout\\.do$", "^/z/[^/]+/logout\\.do$") + : singletonList("^/logout\\.do$"); corsFilter.getFilter().setCorsXhrAllowedUris(allowedUris); corsFilter.getFilter().initialize(); From 0b4dfa3ac7c870a058728bb4ec42130de23e6fab Mon Sep 17 00:00:00 2001 From: Joe Mahady Date: Mon, 10 Aug 2026 09:40:40 +0100 Subject: [PATCH 4/4] Fix concurrent modification in CorsFilter Avoid mutating CorsConfiguration per request by compiling patterns lazily into local lists and swapping them under a lock. Also adds `patternsCompiled` state in `CorsConfiguration` to track when compilation is needed, reducing CPU overhead from recompiling regex on every request. Co-authored-by: Cursor --- .../identity/uaa/zone/CorsConfiguration.java | 25 ++++++++- .../identity/uaa/security/web/CorsFilter.java | 53 ++++++++++++------- 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/CorsConfiguration.java b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/CorsConfiguration.java index 8a893d27a5c..aca18d6b8cb 100644 --- a/model/src/main/java/org/cloudfoundry/identity/uaa/zone/CorsConfiguration.java +++ b/model/src/main/java/org/cloudfoundry/identity/uaa/zone/CorsConfiguration.java @@ -34,14 +34,14 @@ public class CorsConfiguration { * requests. */ private List allowedOrigins = Collections.singletonList(".*"); - private final List allowedOriginPatterns = new ArrayList<>(); + private List allowedOriginPatterns = new ArrayList<>(); /** * A comma delimited list of regular expression patterns that defines which * UAA URIs allow the "X-Requested-With" header in CORS requests. */ private List allowedUris = Collections.singletonList(".*"); - private final List allowedUriPatterns = new ArrayList<>(); + private List allowedUriPatterns = new ArrayList<>(); /** * A comma delimited list of regular expression patterns that define which @@ -60,6 +60,17 @@ public boolean isAllowedCredentials() { return allowedCredentials; } + @com.fasterxml.jackson.annotation.JsonIgnore + private volatile boolean patternsCompiled = false; + + public boolean isPatternsCompiled() { + return patternsCompiled; + } + + public void setPatternsCompiled(boolean patternsCompiled) { + this.patternsCompiled = patternsCompiled; + } + public void setAllowedCredentials(boolean allowedCredentials) { this.allowedCredentials = allowedCredentials; } @@ -84,24 +95,34 @@ public List getAllowedOriginPatterns() { return allowedOriginPatterns; } + public void setAllowedOriginPatterns(List allowedOriginPatterns) { + this.allowedOriginPatterns = allowedOriginPatterns; + } + public List getAllowedOrigins() { return allowedOrigins; } public void setAllowedOrigins(List allowedOrigins) { this.allowedOrigins = allowedOrigins; + this.patternsCompiled = false; } public List getAllowedUriPatterns() { return allowedUriPatterns; } + public void setAllowedUriPatterns(List allowedUriPatterns) { + this.allowedUriPatterns = allowedUriPatterns; + } + public List getAllowedUris() { return allowedUris; } public void setAllowedUris(List allowedUris) { this.allowedUris = allowedUris; + this.patternsCompiled = false; } public int getMaxAge() { diff --git a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java index d45ab6ff864..b4ff7f472a0 100644 --- a/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java +++ b/server/src/main/java/org/cloudfoundry/identity/uaa/security/web/CorsFilter.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -112,8 +113,7 @@ public CorsFilter(final IdentityZoneManager identityZoneManager, public void initialize() { // initialize the configs for default zone for (CorsConfiguration configuration : Arrays.asList(xhrConfiguration, defaultConfiguration)) { - configuration.getAllowedUriPatterns().clear(); - configuration.getAllowedOriginPatterns().clear(); + configuration.setPatternsCompiled(false); compileAllowedOriginsAndUris(configuration, configuration == xhrConfiguration ? "xhr" : "default"); } @@ -377,27 +377,42 @@ private String anchorPattern(String pattern) { } private void compileAllowedOriginsAndUris(CorsConfiguration configuration, String type) { - configuration.getAllowedUriPatterns().clear(); - if (configuration.getAllowedUris() != null) { - for (String allowedUri : configuration.getAllowedUris()) { - try { - configuration.getAllowedUriPatterns().add(Pattern.compile(allowedUri)); - log.debug("URI '%s' is allowed for a %s CORS requests.".formatted(allowedUri, type)); - } catch (PatternSyntaxException patternSyntaxException) { - log.error("Invalid regular expression pattern in cors.{}.allowed.uris: {}", type, allowedUri, patternSyntaxException); + if (configuration.isPatternsCompiled()) { + return; + } + + synchronized (configuration) { + if (configuration.isPatternsCompiled()) { + return; + } + + List uriPatterns = new ArrayList<>(); + if (configuration.getAllowedUris() != null) { + for (String allowedUri : configuration.getAllowedUris()) { + try { + uriPatterns.add(Pattern.compile(allowedUri)); + log.debug("URI '%s' is allowed for a %s CORS requests.".formatted(allowedUri, type)); + } catch (PatternSyntaxException patternSyntaxException) { + log.error("Invalid regular expression pattern in cors.{}.allowed.uris: {}", type, allowedUri, patternSyntaxException); + } } } - } - configuration.getAllowedOriginPatterns().clear(); - if (configuration.getAllowedOrigins() != null) { - for (String allowedOrigin : configuration.getAllowedOrigins()) { - try { - configuration.getAllowedOriginPatterns().add(Pattern.compile(anchorPattern(allowedOrigin))); - log.debug("Origin '%s' is allowed for a %s CORS requests.".formatted(allowedOrigin, type)); - } catch (PatternSyntaxException patternSyntaxException) { - log.error("Invalid regular expression pattern in cors.{}.allowed.origins: {}", type, allowedOrigin, patternSyntaxException); + configuration.setAllowedUriPatterns(uriPatterns); + + List originPatterns = new ArrayList<>(); + if (configuration.getAllowedOrigins() != null) { + for (String allowedOrigin : configuration.getAllowedOrigins()) { + try { + originPatterns.add(Pattern.compile(anchorPattern(allowedOrigin))); + log.debug("Origin '%s' is allowed for a %s CORS requests.".formatted(allowedOrigin, type)); + } catch (PatternSyntaxException patternSyntaxException) { + log.error("Invalid regular expression pattern in cors.{}.allowed.origins: {}", type, allowedOrigin, patternSyntaxException); + } } } + configuration.setAllowedOriginPatterns(originPatterns); + + configuration.setPatternsCompiled(true); } }