Skip to content

Commit 46ab846

Browse files
committed
Mark Observations with CSRF Failures
Closes gh-11993
1 parent d3d8f7d commit 46ab846

File tree

6 files changed

+164
-5
lines changed

6 files changed

+164
-5
lines changed

Diff for: config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java

+19
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.LinkedHashMap;
2121
import java.util.List;
2222

23+
import io.micrometer.observation.ObservationRegistry;
2324
import jakarta.servlet.http.HttpServletRequest;
2425

2526
import org.springframework.context.ApplicationContext;
@@ -29,7 +30,9 @@
2930
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3031
import org.springframework.security.web.access.AccessDeniedHandler;
3132
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
33+
import org.springframework.security.web.access.CompositeAccessDeniedHandler;
3234
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
35+
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
3336
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
3437
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
3538
import org.springframework.security.web.csrf.CsrfFilter;
@@ -221,6 +224,11 @@ public void configure(H http) {
221224
filter.setRequireCsrfProtectionMatcher(requireCsrfProtectionMatcher);
222225
}
223226
AccessDeniedHandler accessDeniedHandler = createAccessDeniedHandler(http);
227+
ObservationRegistry registry = getObservationRegistry();
228+
if (!registry.isNoop()) {
229+
ObservationMarkingAccessDeniedHandler observable = new ObservationMarkingAccessDeniedHandler(registry);
230+
accessDeniedHandler = new CompositeAccessDeniedHandler(observable, accessDeniedHandler);
231+
}
224232
if (accessDeniedHandler != null) {
225233
filter.setAccessDeniedHandler(accessDeniedHandler);
226234
}
@@ -331,6 +339,17 @@ private SessionAuthenticationStrategy getSessionAuthenticationStrategy() {
331339
return csrfAuthenticationStrategy;
332340
}
333341

342+
private ObservationRegistry getObservationRegistry() {
343+
ApplicationContext context = getBuilder().getSharedObject(ApplicationContext.class);
344+
String[] names = context.getBeanNamesForType(ObservationRegistry.class);
345+
if (names.length == 1) {
346+
return context.getBean(ObservationRegistry.class);
347+
}
348+
else {
349+
return ObservationRegistry.NOOP;
350+
}
351+
}
352+
334353
/**
335354
* Allows registering {@link RequestMatcher} instances that should be ignored (even if
336355
* the {@link HttpServletRequest} matches the

Diff for: config/src/main/java/org/springframework/security/config/http/CsrfBeanDefinitionParser.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@
3636
import org.springframework.security.access.AccessDeniedException;
3737
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
3838
import org.springframework.security.web.access.AccessDeniedHandler;
39+
import org.springframework.security.web.access.CompositeAccessDeniedHandler;
3940
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
41+
import org.springframework.security.web.access.ObservationMarkingAccessDeniedHandler;
4042
import org.springframework.security.web.csrf.CsrfAuthenticationStrategy;
4143
import org.springframework.security.web.csrf.CsrfFilter;
4244
import org.springframework.security.web.csrf.CsrfLogoutHandler;
@@ -80,6 +82,8 @@ public class CsrfBeanDefinitionParser implements BeanDefinitionParser {
8082

8183
private String requestHandlerRef;
8284

85+
private BeanMetadataElement observationRegistry;
86+
8387
@Override
8488
public BeanDefinition parse(Element element, ParserContext pc) {
8589
boolean disabled = element != null && "true".equals(element.getAttribute("disabled"));
@@ -160,7 +164,16 @@ private BeanMetadataElement createAccessDeniedHandler(BeanDefinition invalidSess
160164
.rootBeanDefinition(DelegatingAccessDeniedHandler.class);
161165
deniedBldr.addConstructorArgValue(handlers);
162166
deniedBldr.addConstructorArgValue(defaultDeniedHandler);
163-
return deniedBldr.getBeanDefinition();
167+
BeanDefinition denied = deniedBldr.getBeanDefinition();
168+
ManagedList compositeList = new ManagedList();
169+
BeanDefinitionBuilder compositeBldr = BeanDefinitionBuilder
170+
.rootBeanDefinition(CompositeAccessDeniedHandler.class);
171+
BeanDefinition observing = BeanDefinitionBuilder.rootBeanDefinition(ObservationMarkingAccessDeniedHandler.class)
172+
.addConstructorArgValue(this.observationRegistry).getBeanDefinition();
173+
compositeList.add(denied);
174+
compositeList.add(observing);
175+
compositeBldr.addConstructorArgValue(compositeList);
176+
return compositeBldr.getBeanDefinition();
164177
}
165178

166179
BeanDefinition getCsrfAuthenticationStrategy() {
@@ -195,6 +208,10 @@ void setIgnoreCsrfRequestMatchers(List<BeanDefinition> requestMatchers) {
195208
}
196209
}
197210

211+
void setObservationRegistry(BeanMetadataElement observationRegistry) {
212+
this.observationRegistry = observationRegistry;
213+
}
214+
198215
private static final class DefaultRequiresCsrfMatcher implements RequestMatcher {
199216

200217
private final HashSet<String> allowedMethods = new HashSet<>(Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));

Diff for: config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

+29-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import io.micrometer.observation.ObservationRegistry;
2223
import jakarta.servlet.ServletRequest;
2324
import org.w3c.dom.Element;
2425

@@ -106,6 +107,8 @@ class HttpConfigurationBuilder {
106107

107108
private static final String ATT_INVALID_SESSION_URL = "invalid-session-url";
108109

110+
private static final String ATT_OBSERVATION_REGISTRY_REF = "observation-registry-ref";
111+
109112
private static final String ATT_SESSION_AUTH_STRATEGY_REF = "session-authentication-strategy-ref";
110113

111114
private static final String ATT_SESSION_AUTH_ERROR_URL = "session-authentication-error-url";
@@ -211,7 +214,7 @@ class HttpConfigurationBuilder {
211214
private boolean addAllAuth;
212215

213216
HttpConfigurationBuilder(Element element, boolean addAllAuth, ParserContext pc, BeanReference portMapper,
214-
BeanReference portResolver, BeanReference authenticationManager) {
217+
BeanReference portResolver, BeanReference authenticationManager, BeanMetadataElement observationRegistry) {
215218
this.httpElt = element;
216219
this.addAllAuth = addAllAuth;
217220
this.pc = pc;
@@ -226,7 +229,7 @@ class HttpConfigurationBuilder {
226229
createSecurityContextHolderStrategy();
227230
createForceEagerSessionCreationFilter();
228231
createDisableEncodeUrlFilter();
229-
createCsrfFilter();
232+
createCsrfFilter(observationRegistry);
230233
createSecurityPersistence();
231234
createSessionManagementFilters();
232235
createWebAsyncManagerFilter();
@@ -812,9 +815,10 @@ private void createDisableEncodeUrlFilter() {
812815
}
813816
}
814817

815-
private void createCsrfFilter() {
818+
private void createCsrfFilter(BeanMetadataElement observationRegistry) {
816819
Element elmt = DomUtils.getChildElementByTagName(this.httpElt, Elements.CSRF);
817820
this.csrfParser = new CsrfBeanDefinitionParser();
821+
this.csrfParser.setObservationRegistry(observationRegistry);
818822
this.csrfFilter = this.csrfParser.parse(elmt, this.pc);
819823
if (this.csrfFilter == null) {
820824
this.csrfParser = null;
@@ -897,6 +901,14 @@ List<OrderDecorator> getFilters() {
897901
return filters;
898902
}
899903

904+
private static BeanMetadataElement getObservationRegistry(Element httpElmt) {
905+
String holderStrategyRef = httpElmt.getAttribute(ATT_OBSERVATION_REGISTRY_REF);
906+
if (StringUtils.hasText(holderStrategyRef)) {
907+
return new RuntimeBeanReference(holderStrategyRef);
908+
}
909+
return BeanDefinitionBuilder.rootBeanDefinition(ObservationRegistryFactory.class).getBeanDefinition();
910+
}
911+
900912
static class RoleVoterBeanFactory extends AbstractGrantedAuthorityDefaultsBeanFactory {
901913

902914
private RoleVoter voter = new RoleVoter();
@@ -944,4 +956,18 @@ public Class<?> getObjectType() {
944956

945957
}
946958

959+
static class ObservationRegistryFactory implements FactoryBean<ObservationRegistry> {
960+
961+
@Override
962+
public ObservationRegistry getObject() throws Exception {
963+
return ObservationRegistry.NOOP;
964+
}
965+
966+
@Override
967+
public Class<?> getObjectType() {
968+
return ObservationRegistry.class;
969+
}
970+
971+
}
972+
947973
}

Diff for: config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ private BeanReference createFilterChain(Element element, ParserContext pc) {
150150
ManagedList<BeanReference> authenticationProviders = new ManagedList<>();
151151
BeanReference authenticationManager = createAuthenticationManager(element, pc, authenticationProviders);
152152
boolean forceAutoConfig = isDefaultHttpConfig(element);
153+
BeanMetadataElement observationRegistry = getObservationRegistry(element);
153154
HttpConfigurationBuilder httpBldr = new HttpConfigurationBuilder(element, forceAutoConfig, pc, portMapper,
154-
portResolver, authenticationManager);
155+
portResolver, authenticationManager, observationRegistry);
155156
httpBldr.getSecurityContextRepositoryForAuthenticationFilters();
156157
AuthenticationConfigBuilder authBldr = new AuthenticationConfigBuilder(element, forceAutoConfig, pc,
157158
httpBldr.getSessionCreationPolicy(), httpBldr.getRequestCache(), authenticationManager,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.access;
18+
19+
import java.io.IOException;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.Collection;
23+
24+
import jakarta.servlet.ServletException;
25+
import jakarta.servlet.http.HttpServletRequest;
26+
import jakarta.servlet.http.HttpServletResponse;
27+
28+
import org.springframework.security.access.AccessDeniedException;
29+
30+
public final class CompositeAccessDeniedHandler implements AccessDeniedHandler {
31+
32+
private Collection<AccessDeniedHandler> handlers;
33+
34+
public CompositeAccessDeniedHandler(AccessDeniedHandler... handlers) {
35+
this(Arrays.asList(handlers));
36+
}
37+
38+
public CompositeAccessDeniedHandler(Collection<AccessDeniedHandler> handlers) {
39+
this.handlers = new ArrayList<>(handlers);
40+
}
41+
42+
@Override
43+
public void handle(HttpServletRequest request, HttpServletResponse response,
44+
AccessDeniedException accessDeniedException) throws IOException, ServletException {
45+
for (AccessDeniedHandler handler : this.handlers) {
46+
handler.handle(request, response, accessDeniedException);
47+
}
48+
}
49+
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.web.access;
18+
19+
import java.io.IOException;
20+
21+
import io.micrometer.observation.Observation;
22+
import io.micrometer.observation.ObservationRegistry;
23+
import jakarta.servlet.ServletException;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
27+
import org.springframework.security.access.AccessDeniedException;
28+
29+
public final class ObservationMarkingAccessDeniedHandler implements AccessDeniedHandler {
30+
31+
private final ObservationRegistry registry;
32+
33+
public ObservationMarkingAccessDeniedHandler(ObservationRegistry registry) {
34+
this.registry = registry;
35+
}
36+
37+
@Override
38+
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception)
39+
throws IOException, ServletException {
40+
Observation observation = this.registry.getCurrentObservation();
41+
if (observation != null) {
42+
observation.error(exception);
43+
}
44+
}
45+
46+
}

0 commit comments

Comments
 (0)