※実際にあった事例をもとにしていますが、詳しく話せない箇所は伏せたりぼかしたりしているのでその点ご承知おきください。
背景
以下のようなアプリケーションを開発していました。
このアプリケーションでは全ユースケースで最初から認証を必要とするサービスを提供していました。ところが、サービスの新たな顧客が「未認証のユーザー(以降『匿名ユーザー』)を識別する」という要件を出してきました。
そこで、このアプリケーションで Spring Security によるセッション管理を有効にすることで、この要件を実現しようとしました。セッション管理を有効にすれば、全てのリクエストに対してセッション Cookie が発行されるので、匿名ユーザーでも識別できるためです。
起こったこと
認証は Amazon Cognito による JWT ベースの認証であり、このアプリケーションでは全てのリクエストに対して JWT の検証を実施していました。つまり、毎回ログイン処理を行うような動きになります。
ところで、 Spring Security のセッション管理は、セッションフィクセイション攻撃対策のため、既定でログイン時に既存セッションのセッション ID を更新します。
つまり、JWT ベースの認証を実施している状態で Spring Security のセッション管理を有効にすると、全てのリクエストで既存セッションのセッション ID が更新されることになります。
一方で、フロントエンドは Vue.js による SPA であり、非同期に複数の Web API 呼び出しが行われます。つまり、レスポンスの受信タイミングが前後することがあります。
何が起こったかというと、すべてのリクエストでセッション ID が更新される+レスポンスの受信タイミングが前後する → セッション ID の不整合が発生し、エラーが多発しました。
事例での回避策
セッション ID が更新されないように、 Spring Security の設定を変更し、セッション ID が更新されないようにしました( 非推奨 )。
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.sessionManagement((session) -> session
.sessionFixation((sessionFixation) -> sessionFixation
.none()
)
);
return http.build();
}
ただし、これではセッション固定攻撃( セッションフィクセイション : Session Fixation )に対して脆弱になってしまいます。そのため、セッションを使用する機能の Web API を洗い出し、その Web API の呼び出し時のみセッション ID を更新するように実装を追加しました。
なお、これはセッションを使用する機能がそれほど多くなかったため採れた対策です。多くの機能でセッションを使用していた場合、セッション ID 不整合エラーは解消しなかったと思われます。
どうすれば良かったか
Spring Security の既定の設定を緩めることは、セキュリティの観点から極力すべきではありません。この事例では Spring Security がやってくれることを自分で実装することになりましたが、注意深く実装しないとセキュリティホールを作りこんでしまうことになります。
べき論としては、SPA はセッションレスアーキテクチャで作るのが基本であり、匿名ユーザーの識別についても独自に JWT を発行する等の方式で実装するべきでした。
教訓
セッションレス前提のアーキテクチャでうかつにセッションを有効にしようとすると思わぬところで躓くので、最後までセッションレスを貫くのが吉
We Are Hiring!