To path or not to path…

The story…

Jerzy Szymanski
Julius Baer Engineering
4 min readJul 9, 2024

--

Our penetration testing team identified an issue with a Spring Boot application (Spring Boot 2.7.x). The problem was that the application would return the request path itself back to the user if it contained invalid characters. These invalid characters could be a semicolon or any other character that Spring considers unsafe.

This behaviour can be a security concern because the malformed path could potentially be interpreted by the user’s browser as JavaScript code or a request for an external script. If not handled properly by the browser, this could lead to security vulnerabilities.

While Spring Boot offers strong protection against malicious requests by default, it’s important to remember that not every scenario can be automatically configured.

How does Spring framework handle invalid requests?

Spring Boot offers robust out-of-the-box handling for erroneous requests. This means you don’t need to manually configure defences against common attacks such as path traversal or malicious JavaScript injection within path parameters.

The key component of this configuration is the org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration class. This class checks for existing beans of the ErrorController type in the application context. If none are found, it automatically creates an instance of the org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController class to handle error responses.

 @Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

This controller deals with errors for requests rejected very early on in the processing pipeline, specifically by the security filter. Path-related issues are detected by the org.springframework.security.web.firewall.StrictHttpFirewall class. While this class offers some configuration options, it is not fully customisable.

The solution

There are several approaches to address this issue, and overriding the BasicErrorController seemed the most straightforward option in my view.

When composing the error response, the error controller uses a list of attributes defined by the org.springframework.boot.web.servlet.error.DefaultErrorAttributes class. By default, this class includes the request path alongside other attributes such as stack trace and exception details. However, these latter attributes can be removed by configuration, unlike path.

Given our collection of Spring Boot applications, a generic solution was desired. This meant creating a library that used Spring autoconfiguration rather than implementing the fix individually in each application. Consequently, including the library would automatically replace the default Spring controller and fix the flaw.

First step: autoconfiguration magic

The first step, of course, is to tell Spring that there’s something to autoconfigure. This requires creating a file named org.springframework.boot.autoconfigure.AutoConfiguration.imports within the library’s resources directory under META-INF/spring. The file should contain a single line:

com.library.autoconfigure.CustomErrorControllerConfiguration

This line specifies the class responsible for configuring the necessary beans.

Examining the ErrorMvcAutoConfiguration class, we see it creates two beans relevant to this solution: DefaultErrorAttributes and BasicErrorController. While the latter requires an ErrorAttributes type bean (which is, of course, implemented by DefaultErrorAttributes), Spring manages their creation as a set.

Ideally, one might prefer to have more granular control over bean instantiation, rather than relying on Spring’s all-or-nothing approach. Fortunately, there’s a workaround for this limitation.

Second step: autoconfiguration implementation

Crucially, the configuration class needs to be instantiated before the ErrorMvcAutoConfigurationclass. Otherwise, our custom controller won’t be registered in the context. This is why the @AutoConfiguration(before = ErrorMvcAutoConfiguration.class) annotation is used.

While the two below conditionals aren’t strictly necessary for our servlet-based applications, I’ve included them for clarity. The ServerProperties class is enabled because it’s used within this context, similar to the condition on Servlet class.

@AutoConfiguration(before = ErrorMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@EnableConfigurationProperties({ServerProperties.class})
public class CustomErrorControllerConfiguration {
private final ServerProperties serverProperties;
public CustomErrorControllerConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
...

To ensure that my custom beans take precedence, I needed to explicitly define them before ErrorMvcAutoConfiguration was instantiated. This involved creating both the CustomErrorController and the ErrorAttributes beans manually.

   @Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public CustomErrorController customErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new CustomErrorController(errorAttributes, this.serverProperties,
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
}

Third step: controller implementation

The controller implementation itself is rather straightforward. It primarily focuses on the attributes returned to the caller within the response body. Notably, there’s no need to construct the response itself. Instead, the focus lies on specifying what information should not be included in the response. Within the constructor, I opted to restrict the exposure of both the exception and its associated stack trace, which are logged elsewhere.

@Primary
@RestController
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, serverProperties.getError(), errorViewResolvers);
final var errProperties = super.getErrorProperties();
errProperties.setIncludeException(false);
errProperties.setIncludeStacktrace(IncludeAttribute.NEVER);
}
...

Finally, addressing the path removal…

   @Override
protected Map<String, Object> getErrorAttributes(final HttpServletRequest request, final ErrorAttributeOptions options) {
final Map<String, Object> errorAttributes = super.getErrorAttributes(request, options);
errorAttributes.remove("path");
return errorAttributes;
}
}

Conclusion

The Spring Framework excels at streamlining endpoint security for developers. Furthermore, while customising specific behaviours might not always be as straightforward as overriding a single bean, there is invariably a way to tailor functionality to individual requirements.

--

--