Skip to content

Preserve firewall in reactive Cloud Foundry security auto-configuration - #51549

Open
aashikantkumar wants to merge 1 commit into
spring-projects:mainfrom
aashikantkumar:gh-45377
Open

Preserve firewall in reactive Cloud Foundry security auto-configuration#51549
aashikantkumar wants to merge 1 commit into
spring-projects:mainfrom
aashikantkumar:gh-45377

Conversation

@aashikantkumar

Copy link
Copy Markdown

Fixes #45377

Hi team,

This PR addresses an issue where custom ServerWebExchangeFirewall configurations are lost when running reactive applications on Cloud Foundry.

What was happening?
When VCAP_APPLICATION is present, CloudFoundryReactiveActuatorAutoConfiguration used a BeanPostProcessor (WebFilterChainPostProcessor) to wrap WebFilterChainProxy.

The trouble with that approach is that instantiating a new WebFilterChainProxy creates a fresh instance with its private firewall field defaulting to new StrictServerWebExchangeFirewall(). As a result:

Any custom ServerWebExchangeFirewall bean declared by the user was discarded.
Direct customizations (e.g. calling setFirewall(...) on the proxy) were also lost.
Incoming requests were evaluated by the new proxy's strict firewall at the perimeter, rejecting valid requests that rely on customized firewall rules (like matrix parameters or specific URI encodings).
The Fix
Following @wilkinsona's suggestion in #45377, we aligned the reactive configuration with how the servlet side already handles this in IgnoredCloudFoundryPathsWebSecurityConfiguration:

Removed WebFilterChainPostProcessor.
Added a dedicated @order(-1) SecurityWebFilterChain bean matching /cloudfoundryapplication/** with permitAll(), csrf.disable(), and CORS configuration.
This allows Spring Security's native ServerHttpSecurityConfiguration to create and keep the single WebFilterChainProxy. Cloud Foundry routes are given higher precedence to bypass authentication, while the rest of the application's security configuration and the user's custom firewall remain completely untouched.

How this was verified
Added regression tests in CloudFoundryReactiveActuatorAutoConfigurationTests:

customFirewallBeanIsPreserved: Asserts that a custom ServerWebExchangeFirewall @bean stays on WebFilterChainProxy.
directlyConfiguredFirewallIsPreserved: Asserts that firewalls set directly via proxy.setFirewall(...) in a post-processor survive.
customFirewallIsInvokedOnRequests: Verifies runtime execution of firewall.getFirewalledExchange(...).
userSecurityWebFilterChainIsPreserved: Confirms that user-defined SecurityWebFilterChain beans coexist alongside the Cloud Foundry chain with correct ordering.
Ran:

bash
./gradlew :module:spring-boot-cloudfoundry:test
./gradlew :module:spring-boot-cloudfoundry:check
All reactive tests, servlet tests, checkstyle, and Spring JavaFormat checks pass cleanly.

Contributor Checklist
Signed-off-by trailer included in commit for DCO
Code formatted with Spring JavaFormat
Checkstyle checks pass
@author tag added
Unit/slice regression tests added

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Sep 2, 2026

@wilkinsona wilkinsona left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I've left a couple of comments for your consideration.

Comment on lines +244 to +291
@Test
void customFirewallBeanIsPreserved() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
this.contextRunner.withBean(ServerWebExchangeFirewall.class, () -> customFirewall)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
assertThat(proxy).extracting("firewall").isSameAs(customFirewall);
});
}

@Test
void directlyConfiguredFirewallIsPreserved() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
this.contextRunner.withBean(BeanPostProcessor.class, () -> new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
if (bean instanceof WebFilterChainProxy proxy) {
proxy.setFirewall(customFirewall);
}
return bean;
}
})
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
assertThat(proxy).extracting("firewall").isSameAs(customFirewall);
});
}

@Test
void customFirewallIsInvokedOnRequests() {
ServerWebExchangeFirewall customFirewall = mock(ServerWebExchangeFirewall.class);
given(customFirewall.getFirewalledExchange(any()))
.willAnswer((invocation) -> Mono.just(invocation.getArgument(0)));
this.contextRunner.withBean(ServerWebExchangeFirewall.class, () -> customFirewall)
.withPropertyValues("VCAP_APPLICATION:---", "vcap.application.application_id:my-app-id",
"vcap.application.cf_api:https://my-cloud-controller.com")
.run((context) -> {
WebFilterChainProxy proxy = context.getBean(WebFilterChainProxy.class);
MockServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("/some-other-path").build());
proxy.filter(exchange, (ex) -> Mono.empty()).block(Duration.ofSeconds(30));
then(customFirewall).should().getFirewalledExchange(any());
});
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these tests are necessary as the potential to cause problems with the firewall is removed entirely by the new approach. See also the equivalent servlet-based tests where we don't consider the firewall either. Please remove.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes total sense. Since we're no longer touching WebFilterChainProxy, the firewall isn't impacted anymore. I've removed these three tests and cleaned up the unused imports.

@wilkinsona wilkinsona Sep 4, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be renamed as the paths are now permitted rather than being ignored. New tests should be added to test CSRF and cross-origin requests. See cloudFoundryPathsPermittedWithCsrfBySpringSecurity and crossOriginRequestToCloudFoundryPathsPermittedBySpringSecurity in CloudFoundryActuatorAutoConfigurationTests for inspiration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated! I've renamed the test to cloudFoundryPathsPermittedBySpringSecurity and added both cloudFoundryPathsPermittedWithCsrfBySpringSecurity and crossOriginRequestToCloudFoundryPathsPermittedBySpringSecurity mirroring the servlet test suite.

Prior to this commit, `CloudFoundryReactiveActuatorAutoConfiguration` registered a `BeanPostProcessor` (`WebFilterChainPostProcessor`) that intercepted the primary `WebFilterChainProxy` bean and instantiated a second, new `WebFilterChainProxy` wrapping the original. Because constructing a new `WebFilterChainProxy` initializes its private `firewall` field with `new StrictServerWebExchangeFirewall()`, any custom `ServerWebExchangeFirewall` (configured either via a Spring `@Bean` or via `proxy.setFirewall(...)`) was lost.

This commit aligns the reactive Cloud Foundry security configuration with its servlet counterpart (`IgnoredCloudFoundryPathsWebSecurityConfiguration` in `CloudFoundryActuatorAutoConfiguration`). The `BeanPostProcessor` is removed, and a dedicated `@Bean @order(-1) SecurityWebFilterChain` is registered for the path `/cloudfoundryapplication/**` with `permitAll()`, `csrf.disable()`, and CORS support.

Spring Security's `ServerHttpSecurityConfiguration` collects this chain and places it with higher precedence inside the single, global `WebFilterChainProxy`. The existing proxy instance—including its custom firewall, decorators, and settings—remains untouched.

Tests have been added to verify that:
- Cloud Foundry paths are permitted by Spring Security.
- Requests with CSRF and cross-origin headers are permitted through Spring Security.
- Application-defined `SecurityWebFilterChain` beans coexist with the Cloud Foundry chain under correct order.

Closes spring-projectsgh-45377

Signed-off-by: aashikantkumar <aashikantkumar2@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status: waiting-for-triage An issue we've not yet triaged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReactiveCloudFoundryActuatorAutoConfiguration ignores firewall set on the WebFilterChainProxy when postprocessing it

3 participants