6
4

More than 5 years have passed since last update.

Spring Security を利用していると JSESSIONID を URL に付与できなかった

Posted at

JSESSIONID を URL に含めているようなアプリケーションで Spring Security のバージョンアップを行うとエラーが発生するようになった。
その原因について調査したのでまとめる。

え、今時 JSESSIONID を URL に含めることなんかないって?

環境

  • Spring Boot 2.1.6.RELEASE
  • (Spring Security 5.1.5.RELEASE)
  • Thymeleaf 3.0.11.RELEASE

事象を再現させる

事象を再現させるにはいくつか準備が必要。

JSESSIONID を URL で管理する

Cookie が利用可能なブラウザの場合は Cookie を利用して JSESSIONID を管理してしまう。
強制的に URL で管理するようにサーブレットコンテナの設定を変更する。

Spring Boot を用いた場合は以下のように ServletContextInitializer を Bean 定義することで設定できる。

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

URL Rewriting を有効化する

Spring Security では、URL Rewriting を無効化する機能がデフォルトで設定されている。
以下のように、 WebSecurityConfigurerAdapter を継承したクラスで設定を変更する。

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

ログインページを作る

Spring Security がデフォルトで提供しているログインページでは URL Rewriting が利用できないので、ログインページを作る。

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>ユーザー名: <input type="text" name="username"/></label>
    </div>
    <div>
        <label>パスワード: <input type="password" name="password"/></label>
    </div>
    <input type="submit" value="login"/>
</form>
</body>
</html>

さらにこのページを表示するための Controller メソッドを作る。

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

最後に設定を追加する。
ついでに、ログインに利用するユーザ名とパスワードを指定している。

@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");
    }
}

デフォルトで DelegatingPasswordEncoder が利用されるため、パスワードには prefix が必要。今回は平文なので {noop} を付与している。

DelegatingPasswordEncoder について調べたことは以下にまとめている。
https://qiita.com/d-yosh/items/bb52152318391e5e07aa

ログイン後のページを作る

これも html と Controller メソッドを作る。

hello.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Hello</title>
</head>
<body>
<h1>ログインできたよ</h1>
</body>
</html>
@Controller
public class HelloController {

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

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

事象確認

アプリケーションを起動してアクセスするとエラーが発生する。
(正確にはエラーが発生してエラー画面へリダイレクトするが、そこでもエラーが発生してリダイレクトがループしている。)
エラー.png

ログを見ると RequestRefectedException が発生していることが確認できる。

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

原因

Spring Security は HttpFirewall によって、リクエストのチェックを行っており、その実装の 1つである StrictHttpFirewall では、セミコロンを含む URL を拒否するようになっている。
JSESSIONID を URL に含める場合はセミコロンが URL に付与されるため、StrictHttpFirewall によって拒否されて例外が発生している。

Spring Security のバージョンによってデフォルトで利用する HttpFirewall が異なっていて、かつては DefaultHttpFirewall が利用されていた。
このクラスは、URL にセミコロンが含まれていてもリクエストを拒否することはない。
今回はバージョンを挙げた際に、デフォルトの HttpFirewall が変わったことによってエラーが発生するようになってしまった。

対処1

StrictHttpFirewall にはセミコロンを許容するように設定を変更することが可能。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 諸々省略・・・

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

}

対処2

DefaultHttpFirewall を利用するように設定を変更する。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 諸々省略・・・

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

対処確認

対処1 or 2 を施したうえでアクセスすると、ログイン画面が表示できる。
login.png

ユーザ名:user 、パスワード:user でログインすることができる。
logindekita.png

ちなみにログイン前後で JSESSIONID が変わっているのは、Spring Security のセッション固定化攻撃対策が有効になっているから。
https://docs.spring.io/spring-security/site/docs/5.1.6.RELEASE/reference/htmlsingle/#ns-session-fixation

さいごに

セッションを URL で管理してはいけません。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4