To path or not to path…
The story…
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 ErrorMvcAutoConfiguration
class. 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.