spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.7k stars 5.85k forks source link

OAuth2AuthorizationCodeGrantFilter erroneously consumes POST request body with multipart/form-data #15397

Closed LaCoCa closed 4 weeks ago

LaCoCa commented 1 month ago

Hi,

This part (request.getParameterMap()) consume the request body from POST request where we have multipart/form-data:

MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());

Into filter code:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {
    if (matchesAuthorizationResponse(request)) {
        processAuthorizationResponse(request, response);
        return;
    }
    filterChain.doFilter(request, response);
}

private boolean matchesAuthorizationResponse(HttpServletRequest request) {
    MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
    if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
        return false;
    }

Could you help me with this issue?

More info:

My security config:

  config.oauth2Client(oauth2 -> oauth2
    .authorizationCodeGrant(codeGrant -> codeGrant
      .accessTokenResponseClient(this.accessTokenResponseClient())
    )
  );

Here is the part of the stack, before my service, which consume multipart/form-data:

    at org.springframework.security.oauth2.client.web.**OAuth2AuthorizationCodeGrantFilter**.doFilterInternal(OAuth2AuthorizationCodeGrantFilter.java:168) ~[spring-security-oauth2-client-5.7.6.jar:5.7.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.24.jar:5.3.24]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:147) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:223) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:217) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:214) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:181) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346) ~[spring-security-web-5.7.6.jar:5.7.6]
    at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:178)
sjohnr commented 1 month ago

@LaCoCa thanks for reaching out!

I'm unable to determine if there's truly an issue from the information provided. I do see the use of request.getParameterMap() but have not seen an issue with this filter before. Please provide a minimal, reproducible sample so I can look further into this issue.

LaCoCa commented 1 month ago

Hi @sjohnr Just create one rest service like:

  @PostMapping ("/svc/simple/post/request")
  public ResponseEntity<?> create (@RequestPart (value = "srb") @Valid SimpleRequestBody srb,
                                   @RequestPart(value = "uploadedFile", required = false) MultipartFile uploadedFile )
  {
    return ResponseEntity.status(CREATED).build();
  }

Request body bean:

public class SimpleRequestBody
{
  public SimpleRequestBody(String info)
  {
    this.info = info;
  }

  public SimpleRequestBody()
  {
  }

  private String info;

  public String getInfo()
  {
    return info;
  }

  public void setInfo(String info)
  {
    this.info = info;
  }
}

I upload 2 applications with names - AP1 and AP2 The only difference is that AP2 use filter:

public class MySimpleFilter extends OncePerRequestFilter
{

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
  {

    final Map<String, String[]> mp = request.getParameterMap();

Take a look the result:

image

image

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'srb' is not present at org.springframework.web.multipart.support.RequestPartServletServerHttpRequest.(RequestPartServletServerHttpRequest.java:73) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.annotation.RequestPartMethodArgumentResolver.resolveArgument(RequestPartMethodArgumentResolver.java:139) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146) ~[spring-web-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1071) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:964) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.24.jar:5.3.24] at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909) ~[spring-webmvc-5.3.24.jar:5.3.24]

Also in my main application if I exclude OAuth2AuthorizationCodeGrantFilter using custom filter like:

public class SkipOAuth2CodeGrantFilter extends OncePerRequestFilter
{
  private String codeGrantBeanName;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
  {
    if (StringUtils.isNotEmpty(codeGrantBeanName) && RequestMethod.POST.name().equals(request.getMethod())) {
      if (StringUtils.containsAny(request.getContentType(), MULTIPART_FORM_DATA_VALUE)) {
        request.setAttribute(codeGrantBeanName + ALREADY_FILTERED_SUFFIX, "true");
      }
    }
    filterChain.doFilter(request, response);
  }

  public void setCodeGrantBeanName(String codeGrantBeanName)
  {
    this.codeGrantBeanName = codeGrantBeanName;
  }
}

All serves which are POST with multipart/form-data work correctly.

When I remove my filter SkipOAuth2CodeGrantFilter , OAuth2AuthorizationCodeGrantFilter consumes request body and all services with POST and multipart/form throw error like:

org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'srb' is not present at org.springframework.web.multipart.support.RequestPartServletServerHttpRequest.(RequestPartServletServerHttpRequest.java:73) ~[spring-web-5.3.24.jar:5.3.24]

One more info, I cannot reproduce this issue with test using mockMvc. Thit's why I used postman.

And the last one:

/**
 * Returns a java.util.Map of the parameters of this request.
 * 
 * <p>Request parameters are extra information sent with the request.
 * For HTTP servlets, parameters are contained in the query string or
 * **posted form data.**
 *
 * @return an immutable java.util.Map containing parameter names as 
 * keys and parameter values as map values. The keys in the parameter
 * map are of type String. The values in the parameter map are of type
 * String array.
 */
public Map<String, String[]> getParameterMap();
sjohnr commented 1 month ago

@LaCoCa I have attempted to reproduce the issue using the provided REST controller method (POST /svc/simple/post/request) and postman. I made sure the OAuth2AuthorizationCodeGrantFilter was in the filter chain via http.oauth2Client(). However, I am unable to reproduce the issue. Please consider providing a minimal, reproducible sample, or this issue will be closed.

LaCoCa commented 1 month ago

@sjohnr I already show you with examples and workaround. Can you show me screenshots from your test in postman ?

sjohnr commented 4 weeks ago

I am closing this issue as unable to reproduce. If you are able to provide a minimal, reproducible sample that reproduces the issue, we can reopen and investigate further.

LaCoCa commented 4 weeks ago

@sjohnr Can you show me screenshots from your test in postman ?

LaCoCa commented 4 weeks ago

@sjohnr please do not close the issue . Please provide me screenshot from your test. Did you use multipart/form-data ?

jgrandja commented 4 weeks ago

@LaCoCa It is standard process to provide a minimal sample that reproduces the issue if you feel this is a bug. Showing screenshots in Postman is inefficient and results in lost time on both ends without getting to the root issue.

We will wait for the minimal sample that reproduces the issue so that we can efficiently troubleshoot.