Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate trailing slash match and change default value from true to false #28552

Closed
vpavic opened this issue Jun 1, 2022 · 52 comments
Closed
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@vpavic
Copy link
Contributor

vpavic commented Jun 1, 2022

Whether to match to URLs irrespective of the presence of a trailing slash. If enabled a method mapped to "/users" also matches to "/users/".
The default value is true.

Even though this behavior has been long present in Spring, it introduces ambiguity that can (combined with some other choices) easily have consequences in shape of security vulnerabilities. Consider this example:

@SpringBootApplication
@RestController
public class SampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }

    @GetMapping("/resources")
    String resources() {
        return "Hello from /resources";
    }

    @GetMapping("/resources/{id}")
    String resourceById(@PathVariable Long id) {
        return "Hello from /resources/" + id;
    }

    @Bean
    SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .authorizeHttpRequests(requests -> {
                    requests.antMatchers("/resources").hasRole("admin");
                    requests.antMatchers("/resources/**").hasRole("user");
                    requests.anyRequest().denyAll();
                })
                .httpBasic(Customizer.withDefaults())
                .build();
    }

}
spring.security.user.password=password
spring.security.user.roles=user

Default user (with role user) will get 403 attempting to GET /resources but can avoid protection by issuing GET /resources/, which wouldn't be possible with trailing slash matching disabled.

Let me note that I'm aware that using mvcMatchers instead of antMatchers would have prevented this issue but that doesn't change the fact that there are many configurations out there relying on antMatchers and that sometimes antMatchers are simply more suitable for other reasons.

Also, I personally see little real benefit of having trailing slash matching enabled because:

  • if application renders server-side generated views navigation is either way established using hyperlinks
  • if application exposes APIs then even more so it is expected that requests are aligned with the API docs

For places where it's really needed for application to respond to both requests, I'd argue that it's either way better solution to configure redirects instead of application wide ambiguous request mappings.

Please consider making this change for 6.0.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 1, 2022
@sbrannen sbrannen added in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement labels Jun 1, 2022
@sbrannen sbrannen added this to the Triage Queue milestone Jun 1, 2022
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jun 8, 2022

I think most users would would expect that a trailing slash doesn't make a difference. From that perspective, the default is not unreasonable, and more than just HATEOAS style navigation or API guidance, even just manually typing a URL somewhere.

If we change the default, many would look to change it back, so overall it's hard to avoid the need to align Spring Security with with Spring MVC through mvcMatchers. One could argue that it's a safer default but this applies only when used with Spring Security. It's not unsafe by itself.

For configuring redirects, do you mean externally, like in a proxy? That would solve the problem at a different level, before Spring Security and Spring MVC, so I would be more interested in that direction but we can only make it a recommendation, and we can still make such a recommendation independent of this.

@vpavic
Copy link
Contributor Author

vpavic commented Jun 9, 2022

From that perspective, the default is not unreasonable, and more than just HATEOAS style navigation or API guidance, even just manually typing a URL somewhere.

I can see manually typing a URL as a real argument only in case of web apps (not APIs) and even there it's only potentially useful for a few select URLs that are considered to be entry points into the application and are likely to be directly entered by the users.

One could argue that it's a safer default but this applies only when used with Spring Security. It's not unsafe by itself.

This is the part I strongly disagree with - what Spring Security does is nothing special nor unique. You can end up with the same kind of risks with other Java filter based security frameworks (for example, Apache Shiro) or by applying security (authorization) in an external component that sits in front of your Spring application. After all, on a high level, conceptually these all take the same approach.

For configuring redirects, do you mean externally, like in a proxy?

Either externally in a proxy or using something like Tuckey UrlRewriteFilter or even simply using ViewControllerRegistry::addRedirectViewController to add redirects where needed.

What I would like to see in Spring are the defaults that are not ambiguous and are therefore less prone to abuse. When I see a handler annotated with @GetMapping("/api/resources") that it really maps to only what I see, unless I opt into any additional (ambiguous) behavior explicitly. This change together with #23915 would achieve that.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jun 10, 2022

I'm not necessarily disagreeing. I'm merely making the point that by itself, trailing slash is not unsafe. It's only when combined with a proxy or filter that also interprets URLs, where it becomes a problem, and the problem more generally is that there may be differences, which could go both ways.

That said, this is also the reason, I've come to see over time that URL paths should be left alone as is as much as possible, avoiding normalization as much as possible. It's the only way for frameworks to minimize differences and align naturally. This is also why PathPatternParser was designed to not decode the full path in order to match to mappings, unlike AntPathMatcher, doesn't even support suffix patterns, etc.

In any case, the only way to really make a significant difference here I think is to deprecate the trailingSlash option entirely and encourage an alternative solution, like a proxy or filter to redirect. That enforces being more precise about where you want to do that exactly, and it can be done ahead of security decisions. Otherwise the possibility for a mismatch between web and security frameworks remains. It's just too easy to flip the default back and not think about it again.

This is something that we'll discuss as a possibility for 6.0.

@vpavic
Copy link
Contributor Author

vpavic commented Jun 10, 2022

In any case, the only way to really make a significant difference here I think is to deprecate the trailingSlash option entirely and encourage an alternative solution, like a proxy or filter to redirect.

I like that even better. Thanks for the feedback.

@rstoyanchev rstoyanchev self-assigned this Jun 21, 2022
@rstoyanchev rstoyanchev changed the title Disable trailing slash matching by default Deprecate trailing slash match Jun 21, 2022
@rstoyanchev rstoyanchev modified the milestones: Triage Queue, 6.0.0-M5 Jun 21, 2022
@rstoyanchev
Copy link
Contributor

Team decision: we'll go ahead with this. It aligns with various other path matching changes we've made in 5.2 and 5.3 such suffix pattern matching, path segment trimming, and path decoding, to make path matching more transparent and explicit.

@wilkinsona
Copy link
Member

Will the default be changed to false as part of this deprecation?

@rstoyanchev
Copy link
Contributor

Yes, I'm thinking that we might as well and that we'll have to, since otherwise you'd need to set it in order to stop using it. We had the same issue with suffix pattern matching.

@sdeleuze sdeleuze removed the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 27, 2022
@rstoyanchev rstoyanchev changed the title Deprecate trailing slash match Deprecate trailing slash match and change default value from true to false Jun 29, 2022
@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jun 29, 2022

The trailing slash option is now deprecated and set to false in all applicable places. The change applies mainly to annotated controllers, since SimpleUrlHandlerMapping, it turns out, was already set to false by default. Nevertheless, it's now deprecated throughout and to be removed eventually.

@bclozel
Copy link
Member

bclozel commented Jul 1, 2022

I've just found this while browing the code @rstoyanchev :

Should this value be changed as well?

@rstoyanchev
Copy link
Contributor

Looks like it, yes.

davidkopp added a commit to t2-project/devops that referenced this issue Nov 20, 2023
davidkopp added a commit to t2-project/uibackend that referenced this issue Nov 20, 2023
@iPasalski
Copy link

what is the deprecation period for PathMatchConfigurer#setUseTrailingSlashMatch ?
when setUseTrailingSlashMatch(true) it would work also for actuator api (i.e. actuator/env vs actuator/env/)?

@divStar
Copy link

divStar commented Nov 26, 2023

Just to add to my initial response: in our project we have probably around 200 endpoints and 3 frontends, which access these endpoints in various manners (we hadn't used unified Feign Clients back when we started out).

This change means, that we have to adjust our endpoints in so they explicitly define the endpoint with a trailing slash and one without. We have to adjust all integration tests for all endpoints and we even should adjust all end-to-end tests in multiple platforms. This is easily 10+ business days of work for just this useless change. I'd resort to using setUseTrailingSlashMatch(true), but for some reason this method is marked as deprecated.

Isn't it possible to allow people, who did not pay 100% attention to the trailing slashes, opt-out of this "more security" aspect? We have implemente a few other means to strengthen our API security, so opting out of this thing would be okay.

@aka5h2017
Copy link

Thanks Spring team! I spent an entire day on figuring out why the API stopped working.

You guys do great work with the framework; but this boggles my mind, how can such a decision be made?

@dpozinen
Copy link

dpozinen commented Dec 15, 2023

We have literally hundreds of clients across dozens of environments, this will stall the adoption of SB3 for months and months and will undoubtedly cause issues even with all the due diligence in the world. Absolutely crazy change.
I whole heartedly support #31366 and especially the logging of such cases

@KeatsPeeks
Copy link

We have literally hundreds of clients across dozens of environments, this will stall the adoption of SB3 for months and months and will undoubtedly cause issues even with all the due diligence in the world. Absolutely crazy change. I whole heartedly support #31366 and especially the logging of such cases

Spring upgrades have always been a pain, even between minor versions (remember "allow-bean-definition-overriding" ?).
You can use the deprecated option, it works (until it won't).

cmenon12 added a commit to cmenon12/sporty-shoes-shopping that referenced this issue Apr 5, 2024
cmenon12 added a commit to cmenon12/sporty-shoes-shopping that referenced this issue Apr 5, 2024
…quests with trailing slashes (#14)

* Create navbar link and GET request to change password

* Create PasswordEncoderService

* Finish implementing the password change form

* Don't encode password until it needs to be saved

* Create admin page to view all users

* Reroute requests with a trailing slash

Used https://www.baeldung.com/spring-boot-3-url-matching

An interesting read spring-projects/spring-framework#28552

* Use URLs without trailing slash as default

* Create search functionality for users
@GabrielBB
Copy link

This was a horrible update. Should have been an optional property in application.yml with default false

@ndrscodes
Copy link

I really support different solutions proposed here, such as #31366. As of now, in nearly every project, we're having to fall back to @SuppressWarnings("deprecated").

bclozel pushed a commit that referenced this issue May 14, 2024
Prior to this commit, trailing slash matching was disabled by default in
Spring MVC with gh-28552. `StandaloneMockMvcBuilder` was not changed as
a result and still had the trailing slash match option enabled.

This commit aligns the defaults in `StandaloneMockMvcBuilder` to better
reflect the expected behavior in tests.

Closes gh-32796
@SebasAnasco1517

This comment was marked as off-topic.

@dingshuangxi888
Copy link

dingshuangxi888 commented May 17, 2024

This issue can be resolved by adding the following Filter:

@Component
public class ServletRequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) request) {
            @Override
            public String getRequestURI() {
                String requestURI = super.getRequestURI();
                if (StringUtils.endsWith(requestURI, "/")) {
                    return StringUtils.removeEnd(requestURI, "/");
                }
                return requestURI;
            }
        };
        chain.doFilter(requestWrapper, response);
    }
}

@rstoyanchev
Copy link
Contributor

There is a now a concrete proposal for a similar Filter under #31366.

@soc
Copy link

soc commented Jul 23, 2024

@MartinHaeusler I'm sorry you went through this. Maybe we can improve the documentation to avoid the same issue for other developers. Can you share at which documentation you were looking at while working on this?

Did this ever happen?

I expected some kind of information of this on https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x but neither trailing, slash, nor 404 brings up anything.

@bclozel
Copy link
Member

bclozel commented Jul 23, 2024

@soc thanks for the reminder, I've just added something here: https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#web-applications-1

@soc
Copy link

soc commented Jul 23, 2024

@bclozel Thanks, I think that will be helpful to many people!

@ndrscodes
Copy link

Though i really appreciate the addition to the wiki, i still think that some kind of default filter should be provided by the framework. Even if it's not included in the filterchain by default, it would still make life a lot easier for this very common use-case.

@bclozel
Copy link
Member

bclozel commented Jul 23, 2024

@ndrscodes it's already implemented - please provide feedback directly on the issue when you'll test it: #31366

@Myrmod
Copy link

Myrmod commented Nov 27, 2024

This issue can be resolved by adding the following Filter:

@Component
public class ServletRequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper((HttpServletRequest) request) {
            @Override
            public String getRequestURI() {
                String requestURI = super.getRequestURI();
                if (StringUtils.endsWith(requestURI, "/")) {
                    return StringUtils.removeEnd(requestURI, "/");
                }
                return requestURI;
            }
        };
        chain.doFilter(requestWrapper, response);
    }
}

Another variation since it took me a while to figure out how to apply the previous example:

package com.example.myApp.Filters;

import java.io.IOException;

import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

@Component
public class TrailingSlashFilter implements Filter {

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {

    HttpServletRequestWrapper requestWrappper = new HttpServletRequestWrapper((HttpServletRequest) request) {
      @Override
      public String getRequestURI() {
        String requestURI = super.getRequestURI();

        return StringUtils.trimTrailingCharacter(requestURI, "/".toCharArray()[0]);
      }
    };

    chain.doFilter(requestWrappper, response);
  }

}

@rstoyanchev
Copy link
Contributor

@Myrmod we support this now. Please, see #31366.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests