[JAVA] JSESSIONID could not be assigned to the URL when using Spring Security

An error will occur when upgrading Spring Security in an application that includes JSESSIONID in the URL. I investigated the cause, so I will summarize it.

Well, why not include JSESSIONID in the URL now?

environment

Reproduce the event

Some preparation is required to reproduce the event.

Manage JSESSIONID by URL

In the case of a browser that can use cookies, JSESSIONID is managed by using cookies. Change the settings of the Servlet container to force management by URL.

When using Spring Boot, it can be set by defining ServletContextInitializer as Bean as follows.

@Bean
public ServletContextInitializer servletContextInitializer() {
    return servletContext -> servletContext.setSessionTrackingModes(EnumSet.of(SessionTrackingMode.URL));
}

Enable URL Rewriting

Spring Security has a default feature that disables URL Rewriting. Change the settings in the class that inherits WebSecurityConfigurerAdapter as shown below.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().enableSessionUrlRewriting(true);
    }
}

Make a login page

URL Rewriting is not available on the login page provided by Spring Security by default, so create a login page.

login.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    <div>
        <label>username: <input type="text" name="username"/></label>
    </div>
    <div>
        <label>password: <input type="password" name="password"/></label>
    </div>
    <input type="submit" value="login"/>
</form>
</body>
</html>

In addition, create a Controller method to display this page.

@Controller
public class HelloController {
    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

Finally, add the settings. By the way, the user name and password used for login are specified.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/login")
                .and()
                .sessionManagement().enableSessionUrlRewriting(true);
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("{noop}user").roles("ADMIN");
    }
}

By default, DelegatingPasswordEncoder is used, so the password must have a prefix. Since it is plaintext this time, {noop} is added.

The research on DelegatingPasswordEncoder is summarized below. https://qiita.com/d-yosh/items/bb52152318391e5e07aa

Create a page after login

This also creates an html and Controller method.

hello.html


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<h1>I was able to log in</h1>
</body>
</html>
@Controller
public class HelloController {

    @GetMapping("/")
    public String index() {
        return "hello";
    }

    @GetMapping("/login")
    public String login() {
        return "login";
    }
}

Event confirmation

An error occurs when I start the application and access it. (To be exact, an error occurs and redirects to the error screen, but an error also occurs and the redirect loops.) エラー.png

If you look at the log, you can see that RequestRefectedException has occurred.

org.springframework.security.web.firewall.RequestRejectedException: The request was rejected because the URL contained a potentially malicious String ";"

Cause

Spring Security uses HttpFirewall to check for requests, and one of its implementations, StrictHttpFirewall, rejects URLs that contain semicolons. If you include the JSESSIONID in the URL, a semicolon is added to the URL, which is rejected by StrictHttpFirewall and throws an exception.

The HttpFirewall used by default differs depending on the version of Spring Security, and in the past, DefaultHttpFirewall was used. This class does not reject a request if the URL contains a semicolon. This time, when I listed the version, the error started to occur because the default HttpFirewall changed.

Action 1

You can change the setting of StrictHttpFirewall to allow semicolons.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //Various omissions ...

    @Override
    public void configure(WebSecurity web) throws Exception {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        firewall.setAllowSemicolon(true);
        web.httpFirewall(firewall);
    }

}

Action 2

Change the setting to use DefaultHttpFirewall.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //Various omissions ...

    @Override
    public void configure(WebSecurity web) throws Exception {
        DefaultHttpFirewall firewall = new DefaultHttpFirewall();
        web.httpFirewall(firewall);
    }
}

Confirmation

Action 1 or 2 can be performed before accessing, and the login screen can be displayed. login.png

You can log in with username: user and password: user. logindekita.png

By the way, the JSESSIONID changes before and after login because Spring Security's session fixation attack countermeasures are enabled. https://docs.spring.io/spring-security/site/docs/5.1.6.RELEASE/reference/htmlsingle/#ns-session-fixation

at the end

Sessions should not be managed by URL.

Recommended Posts

JSESSIONID could not be assigned to the URL when using Spring Security
Story when migration could not be done
Things to be aware of when using devise's lockable
The story that the forced update could not be implemented
Static resources could not be distributed from WebFlux when @EnableWebFlux was granted in Spring Boot
Deploy Spring Boot applications to Heroku without using the Heroku CLI
The case that @Autowired could not be used in JUnit5
Response header may not be output correctly in Spring Security 4.1
[Resolved in 5.2.1] Spring Security 5.2.0.RELEASE has incompatibilities not mentioned in the release notes
What to do when the changes in the Servlet are not reflected
Things to note when using Spring AOP in Jersery resource classes
Spring should be suspected when Rails commands do not work properly
Easy way to create a mapping class when using the API
Note that Insert could not be done with Spring Data JDBC
The story of stopping because the rails test could not be done
How to solve the unknown error when using slf4j in Java
The story that the build error did not stop when using Eclipse 2020
[Rails 5] How to display the password change screen when using devise
Correspond to "error that basic authentication does not pass" in the test code "The story that could not be done"
How to solve the problem that it is not processed normally when nesting beans in Spring Batch