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 が利用できないので、ログインページを作る。
<!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 メソッドを作る。
<!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";
}
}
事象確認
アプリケーションを起動してアクセスするとエラーが発生する。
(正確にはエラーが発生してエラー画面へリダイレクトするが、そこでもエラーが発生してリダイレクトがループしている。)
ログを見ると 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 を施したうえでアクセスすると、ログイン画面が表示できる。
ユーザ名:user 、パスワード:user でログインすることができる。
ちなみにログイン前後で JSESSIONID が変わっているのは、Spring Security のセッション固定化攻撃対策が有効になっているから。
https://docs.spring.io/spring-security/site/docs/5.1.6.RELEASE/reference/htmlsingle/#ns-session-fixation
さいごに
セッションを URL で管理してはいけません。