はじめに
以前、Spring Boot と Spring Security のデフォルト設定や対策機能について、記事にまとめました。
これらの設定や機能がどの程度効果を発揮するかを確認するためには、実際にアプリケーションを動作させて個別に検証する必要があります。
そこで、Spring Boot と Spring Security を使用して簡単な TODO アプリケーションを作成し、IPA の『ウェブ健康診断仕様』に基づくセキュリティ診断を行いました。その結果をまとめましたので、ご紹介します。
なお、本記事の内容は、Web アプリケーション開発初心者である筆者の知識と経験に基づいています。そのため、解釈に誤りがある可能性があることをご了承ください。
あくまで一初心者の学習過程の共有ではありますが、同じような方々にとって参考になる点があれば幸いです。
環境
- Java 17
- Spring Boot 3.3.1
- spring-boot-starter-web
- spring-boot-starter-security
- spring-boot-starter-thymeleaf
- mybatis-spring-boot-starter
- OWASP ZAP 2.15.0
診断は Spring Boot アプリを CloudNative Buildpacks
を用いてコンテナ化し、ローカル環境の Docker
で起動させて実施しました。
また、ブラウザ上での改変が難しいものに関しては、OWASP ZAP
を利用しました。
IPA 『ウェブ健康診断仕様』について
『ウェブ健康診断仕様』は、独立行政法人情報処理推進機構 (IPA) が提供する、ウェブサイトで基本的な脆弱性対策ができているかを確認するための資料です。
この資料には、危険度の高い脆弱性など 13 の診断項目が含まれており、各項目の検出パターンと脆弱性有無の判定基準が解説されています。この診断を実施することで、ウェブサイトの現状の安全性を把握することができます。
しかし、この診断はあくまで基本的な対策ができているかどうかを評価するものであり、診断項目は必要最小限に抑えられています。そのため、脆弱性が検出されなかった場合でも、完全な安全性が保証されるわけではないことに注意が必要です。
詳細については、IPA のサイトで公開されている『ウェブ健康診断仕様』を参照してください。
なお、IPA では『安全なウェブサイトの作り方』や『安全な SQL の呼び出し方』も公開されています。
脆弱性の原因を根本から解決する方法や、攻撃による影響を低減する対策が詳しく解説されています。ウェブ健康診断仕様と併せて参照することで、より包括的なセキュリティ対策を学ぶことができるので、ぜひご確認ください。
アプリケーションについて
診断に使用するアプリは、NTT 提供の「Macchinetta Server Framework (1.x) Development Guideline」のチュートリアルを基に作成しました。
このガイドラインでは、Spring
や Spring MVC
、MyBatis
を中心としたフルスタックフレームワークを利用して、保守性の高い Web アプリケーション開発をするためのベストプラクティスを紹介しています。
今回診断に使うアプリは、診断を体験するために必要な最低限の機能を備えつつ、簡潔に説明できる構造にしたかったので、こちらのガイドラインを利用しました。
具体的には、以下の 2 つのチュートリアルで作成するアプリを組み合わせ、いくつか変更を加えて作成しました。
▼ 11.1. チュートリアル(Todo アプリケーション) — Macchinetta Server Framework (1.x) Development Guideline 1.9.1.RELEASE documentation
▼ 11.4. Spring Security チュートリアル — Macchinetta Server Framework (1.x) Development Guideline 1.9.1.RELEASE documentation
本アプリのソースコードは以下のリポジトリで公開しています。
詳細については、上記のサイトをご覧いただければと思いますが、ここでも簡単にご説明します。
アプリケーションの概要
以下の機能を持つ、認証機能付き TODO 管理アプリケーションです。
- ユーザー名とパスワードを使用したログインとログアウト
- アカウント情報の表示
- TODO の一覧表示、登録、完了、削除
要件
このアプリには、以下のような要件があります。
-
アクセス制御
- TODO リストページとアカウント情報ページは、ログインしないと閲覧できない
- ログイン中のユーザーでも、自身のアカウント情報以外は閲覧できない
-
TODO 管理
- 新規 TODO の登録時には文字数制限 (1-30 文字) を設ける
- 未完了の TODO は 5 件までしか登録できない
- 完了済みの TODO は再度完了操作ができない
-
アカウント管理
- ログイン処理では、データベースに格納済みのアカウントを使用する
画面 (機能)
アプリケーションの処理仕様と画面遷移は以下の通りです。
項番 | プロセス名 | HTTP メソッド | パス | 認証 |
---|---|---|---|---|
1 | ホームページ表示 | GET | / | |
2 | ログインフォームページ表示 | GET | /login/loginForm | |
3 | ログイン | POST | /login | |
4 | ログアウト | POST | /logout | |
5 | アカウント情報ページ表示 | GET | /account | 必須 |
6 | TODO リストページ表示 | GET | /todo/list | 必須 |
7 | TODO 作成 | POST | /todo/create | 必須 |
8 | TODO 完了 | POST | /todo/finish | 必須 |
9 | TODO 削除 | POST | /todo | 必須 |
チュートリアルからの主な変更点
- Spring MVC から Spring Boot へ変更
- ビルドツールを Maven から Gradle へ変更
- データベースを H2 から MySQL へ移行
- リポジトリ実装には MyBatis3 を使用
なお、今回の検証は、主に以下の 2 つを目的としています。
- デフォルト設定状態の Spring Boot・Spring Security アプリケーションの安全性を確認すること
- IPA の「ウェブ健康診断仕様」の実施方法を実際に体験すること
そのため、SpringBoot や SPring Security が標準で提供するセキュリティ機能の範囲が明確になるように、例外ハンドリングやアカウント管理については実装していません。
診断手順
今回は、以下のような手順で診断を進めました。
- 全てのエンドポイントをリストアップ
- 各エンドポイントのリクエストパラメータとレスポンスを確認
- 各エンドポイントの性質を『診断対象画面 (機能) 表』と照らし合わせる
- 『診断対象脆弱性表』を参照し、可能性のある脆弱性を特定インジェクション
- ウェブ健康診断仕様に基づいて、特定された脆弱性について診断を実施
診断対象画面 (機能) 表
画面 (機能) 分類 | 定義 |
---|---|
ログイン | ユーザ ID とパスワードを入力する等して認証を行う画面 |
ログアウト | 認証状態を廃棄するための機能 |
パスワード変更 | ユーザが自分のパスワードを変更する画面 |
入力内容確認 | ユーザが入力した値を次の画面で表示し、確認できるようになっている画面 |
DB 更新 | データの新規登録や変更により、データベースに更新処理を行っていると想定される画面 |
DB アクセス | 検索機能やデータ登録・参照機能等、SQL を利用していると想定される画面 |
エラー | エラー表示に特化した画面 |
ファイル名 | ファイル名と想定されるパラメータを引き回している画面 |
Cookie | 「Set-Cookie」ヘッダによって Cookie が設定されている画面 |
リダイレクト | 「Location」ヘッダや「<meta http-equiv="Refresh" ...」タグによって他の画面に遷移している画面 |
メール送信 | アプリケーションがメールを送信している画面 |
認可制御有 | 情報アクセスのための認可システムが実装されている箇所 |
診断対象脆弱性表
脆弱性 | 対象画面 (機能) 分類 |
---|---|
SQL インジェクション | DB アクセス |
XSS (クロスサイト・スクリプティング) | 入力内容確認, エラー |
CSRF (クロスサイト・リクエスト・フォージェリ) | DB 更新, パスワード変更, メール送信 (※ 注 1) |
OS コマンド・インジェクション | ファイル名, メール送信 |
ディレクトリ・リスティング | 任意の箇所 (※ 注 2) |
メールヘッダ・インジェクション | メール送信 |
パス名パラメータの未チェック / ディレクトリ・トラバーサル | ファイル名 (※ 注 3) |
意図しないリダイレクト | リダイレクト |
HTTP ヘッダ・インジェクション | Cookie, リダイレクト |
認証 | ログイン, ログアウト |
セッション管理の不備 | ログイン, ログアウト |
認可制御の不備、欠落 | 認可制御 |
※ 注 1: メール送信機能における CSRF 検査は、ログイン後の状態でメール送信機能が利用可能な場合にのみ行う
※ 注 2: 特に調査対象の基準を設けていないため、必要に応じて任意の箇所で診断を行う
※ 注 3: ファイルアクセスが想定される画面、ファイル名を想起させるパラメータがある場合に行う
診断実施
Spring Framework は多くのセキュリティ機能をデフォルトで提供しています。そのため、今回診断を行ったアプリケーションでは、多くの項目で同様の結果が得られました。
これらの項目については、最初に診断方法、結果、実装などを説明し、以降は重複を避けるために省略しています。
診断未実施エンドポイント
以下のエンドポイントについては、該当する脆弱性がないと判断したため、診断を実施しませんでした。
1. ホームページ表示
- エンドポイント: GET 『/』
- パラメータ: なし
2. ログインフォームページ表示
- エンドポイント: GET 『/login/loginForm』
- パラメータ: なし
- 機能説明: ログインフォームを表示する
ログイン / ログアウト エンドポイント
3. ログイン
- エンドポイント: POST 『/login』
-
パラメータ:
- cookie: JSESSIONID
- form: _csrf, password, username
- 機能説明: ログインフォームで入力されたユーザー名とパスワードを使って認証する (Spring Security が行う)
- 診断対象画面 (機能) : ログイン, DB アクセス, Cookie
- 診断対象脆弱性: SQL インジェクション, CSRF, 認証, セッション管理の不備, HTTP ヘッダインジェクション
4. ログアウト
- エンドポイント: POST 『/logout』
-
パラメータ:
- cookie: JSESSIONID
- form: _csrf
- 機能説明: ログアウトする (Spring Security が行う)
- 診断対象画面 (機能) : ログアウト, Cookie
- 診断対象脆弱性: CSRF, 認証, セッション管理の不備
■ SQL インジェクション
検出方法: 『ログインフォームページ』でユーザー名に '
(シングルクオート 1 つ) を入力して、Login ボタンを押す
結果: ログイン失敗メッセージを含む『ログインフォームページ』が再表示される
実装:
PreparedStatement が使用される #{...}
構文を使用しているため、安全にクエリを実行することができています。
<select id="findByUsername" parameterType="String" resultMap="accountResultMap">
SELECT
username,
password,
first_name,
last_name
FROM
account
WHERE
username = #{username};
</select>
本アプリは適切なエラーハンドリングを実装していないため、試しに #{username}
を PreparedStatement が使用されない '${username}'
に変更して実行してみると、以下のようなエラーメッセージがクライアントに表示されてしまいます。
エラーメッセージにはデータベース構造やアプリケーションの内部情報など、攻撃者にとって有用な情報が含まれています。完全にアウトですね。
セキュリティを向上させるためには、以下のような対策が重要です。
- 正しく PreparedStatement を使用する (
${...}
ではなく#{...}
を使用する) - 適切なエラーハンドリングを実装し、クライアントには一般的なエラーメッセージのみを表示する
- 詳細なエラー情報はサーバーのログにのみ記録し、クライアントには返さない
※ その他の検出方法について
本アプリでは検索機能を実装していないため、'and'a'='a
や and 1=1
を用いた診断は実施しませんでした。
■ CSRF
検出方法 1: OWASP ZAP でブレークポイントをセット後、『ログインフォームページ』の Login ボタンを押す。ブレークポイントでは、CSRF トークンパラメータを削除して、リクエストを再開する。
検出方法 2: OWASP ZAP でブレークポイントをセット後、『ログインフォームページ』の Login ボタンを押す。ブレークポイントでは、CSRF トークンパラメータの値を別ユーザーのものに変更して、リクエストを再開する。
結果: Whitelabel Error Page (type=Forbidden, status=403) が表示される
実装:
Spring Security は CSRF トークンを自動生成して検証する機能を提供しています。
この機能はデフォルトで有効化されており、POST
、PUT
、DELETE
、PATCH
メソッドのリクエストでは CSRF トークンが自動的に検証されます。
Thymeleaf テンプレートエンジンを使用する場合は、フォームで th:action
属性を使用することで、CSRF トークンを自動で挿入できます。
<form th:action="@{/login}" method="post">
<!-- 省略 -->
</form>
この設定により、フォームには自動的に _csrf
という名前の hidden フィールドが追加され、実際には以下のような HTML が生成されます。
<input type="hidden" name="_csrf" value="d3tjv5bnb8gHkzvECBE1ptlBmznENS-iVS53jZ5sOid1-95wEU1TjaaBWPAqol72PzwBlrpytlv2DUuPNEpD66ZdDhRMzLxE">
自動挿入された各セッションごとに一意な CSRF トークン値はサーバー側で検証されるため、正規のフォームからのリクエストであることが確認できます。
■ 認証
検出方法: 『ログインフォームページ』で誤ったユーザー名またはパスワードを入力して、Login ボタンを押す
結果: ログイン失敗メッセージを含む『ログインフォームページ』が再表示される (メッセージにはユーザー名とパスワードのどちらが間違っていたかの具体的な記述はない)
実装:
ここでは、Spring Security によって自動的に設定された SPRING_SECURITY_LAST_EXCEPTION
属性から認証エラー時に発生した例外オブジェクトを取得し、そのメッセージを表示しています。
<div th:if="${param.containsKey('error')}" th:with="exception = ${SPRING_SECURITY_LAST_EXCEPTION} ?: ${session[SPRING_SECURITY_LAST_EXCEPTION]}">
<ul th:if="${exception != null}" class="alert alert-error">
<li th:text="${exception.message}"></li>
</ul>
</div>
このエラーメッセージは src/main/resources/messages.properties
でカスタマイズすることも可能です。
AbstractUserDetailsAuthenticationProvider.badCredentials=ログインに失敗しました。ユーザー名またはパスワードを確認してください。
※ その他の検出方法について
本アプリではパスワード変更機能やアカウント管理機能を実装していないため、その他の検出方法を用いた診断は未実施とします。
■ セッション管理の不備
検出方法 1: 『ログインフォームページ』で Login ボタンを押す前と後に、ブラウザのデベロッパーツールで JSESSIONID
の値を確認する
-
結果: 応答ヘッダー (Response Headers) の
Set-Cookie
項目にログイン前と異なるJSESSIONID
が設定されている
検出方法 2: ブラウザの Cookie を無効にした状態で、『ログインフォームページ』の Login ボタンを押す
- 結果: Whitelabel Error Page (type=Forbidden, status=403) が表示される
実装:
Spring Boot および Spring Security は、以下のようにセッション管理に関する多くの機能をデフォルトで提供しています。そのため、基本的なセキュリティ対策については特別な設定を行わなくても自動的に適用されます。
- ログイン成功時に新しいセッション ID を生成する
- 自動的にサーブレットコンテナによる標準的なセッション管理機構が利用される
- HTTPS プロトコル使用時、自動的に Cookie に Secure 属性が付与される
- デフォルトで URL リライティングが無効化されており、セッション ID を URL に埋め込むことは推奨されていない
※ その他の検出方法について
その他の検出方法を用いた診断に関しては、上記のデフォルト動作に従うため、詳細は省略します。
■ HTTP ヘッダインジェクション
Spring Boot では、JSESSIONID
はサーブレットコンテナによって生成され、Cookie に格納されます。セッション管理において、開発者が Cookie を直接操作することはないため、ここでは HTTP ヘッダインジェクションに関する診断を未実施としました。
データ表示 エンドポイント
5. アカウント情報ページ表示
- エンドポイント: GET 『/account』
- パラメータ: cookie: JSESSIONID
- 機能説明: ログイン中のユーザーのアカウント情報を表示する
- 診断対象画面 (機能) : DB アクセス, 認可制御有
- 診断対象脆弱性: SQL インジェクション, 認可制御の不備、欠落
6. TODO リストページ表示
- エンドポイント: GET 『/todo/list』
- パラメータ: cookie: JSESSIONID
- 機能説明: TODO 一覧を表示する
- 診断対象画面 (機能) : DB アクセス
- 診断対象脆弱性: SQL インジェクション
■ 認可制御の不備、欠落
本アプリは、セキュリティ要件として「ログイン中のユーザーは自身のアカウント情報のみを閲覧でき、他のユーザーの情報にはアクセスできない」という制約を設けています。この要件を満たすため、以下のような実装を行っています。
@GetMapping
public String view(@AuthenticationPrincipal SampleUserDetails userDetails, Model model) {
Account account = userDetails.getAccount();
model.addAttribute(account);
return "account/view";
}
リクエストハンドラで @AuthenticationPrincipal
アノテーションを使用してログイン中ユーザーの情報を直接取得しています。これにより、ユーザーが URL やリクエストパラメータを操作して他のユーザーの情報にアクセスすることを防いでいます。
結果として、URL 操作によってユーザーに実行権限のない機能を実行できるようなエンドポイントは存在しないため、この点に関する診断は実施していません。
データ操作 エンドポイント
7. TODO 作成
- エンドポイント: POST 『/todo/create』
-
パラメータ:
- cookie: JSESSIONID
- form: _csrf, todoTitle
- 機能説明: TODO を作成し、処理終了後は TODO リストページへリダイレクトする
- 診断対象画面 (機能) : DB 更新, リダイレクト
- 診断対象脆弱性: SQL インジェクション, XSS, CSRF, 意図しないリダイレクト, HTTP ヘッダ・インジェクション
8. TODO 完了
- エンドポイント: POST 『/todo/finish』
-
パラメータ:
- cookie: JSESSIONID
- form: _csrf, todoId
- 機能説明: TODO を完了し、処理終了後は TODO リストページへリダイレクトする
- 診断対象画面 (機能) : DB 更新, リダイレクト
- 診断対象脆弱性: SQL インジェクション, CSRF, 意図しないリダイレクト, HTTP ヘッダ・インジェクション
9. TODO 削除
- エンドポイント: GET 『/todo/delete』
-
パラメータ:
- cookie: JSESSIONID
- form: _csrf, todoId
- 機能説明: TODO を削除し、処理終了後は TODO リストページへリダイレクトする
- 診断対象画面 (機能) : DB 更新, リダイレクト
- 診断対象脆弱性: SQL インジェクション, CSRF, 意図しないリダイレクト, HTTP ヘッダ・インジェクション
■ XSS
検出方法 1: 『TODO リストページ』の TODO タイトルに '>"><hr>
入力して、Create Todo ボタンを押す
- 結果: 新規に TODO を作成した後、特殊文字がエスケープされて、安全な状態で画面に表示される
-
実装: Tymeleaf の
${...}
構文を使用しているため、HTML エスケープされた状態で表示されます。/todo/list.html<li th:each="todo : ${todos}"> <span th:class="${todo.finished} ? 'strike'" th:text="${todo.todoTitle}"> Send a e-mail </span> <!-- 省略 --> </li>
試しに HTML エスケープを行わない
th:utext
に変更して実行してみると、以下のように表示されます。HTML がエスケープされずに解釈されたため、
<hr>
タグが実際の HTML 要素として処理され、水平線が表示されてしまっています。この違いから、th:text
によるエスケープは正常に機能していると判断できますね。
検出方法 2: 『TODO リストページ』の TODO タイトルに '>"><script>alert(document.cookie)</script>
入力して、Create Todo ボタンを押す
- 結果: 文字数制限によりバリデーションエラーが発生する
-
実装: TODO 作成のリクエストを受け取るメソッドでは、要件に従って
@Validated
アノテーションを使用したバリデーションを実施しています。TodoController.java@PostMapping("create") public String create(@Validated({TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } // ... (省略) ... }
TodoForm.javapublic class TodoForm implements Serializable { public interface TodoCreate {} public interface TodoFinish {} public interface TodoDelete {} @NotNull(groups = {TodoFinish.class, TodoDelete.class}) private String todoId; @NotNull(groups = {TodoCreate.class}) @Size(min = 1, max = 30, groups = {TodoCreate.class}) private String todoTitle; // ... (省略) ... }
@Validated
アノテーションは、Spring のバリデーション機能を有効にし、フォームデータの入力チェックを自動的に実行します。このバリデーションの結果はBindingResult
オブジェクトに格納されます。
■ 意図しないリダイレクト・HTTP ヘッダ・インジェクション
いずれのエンドポイントもリダイレクト先が固定のパス (/todo/list) となっており、外部からの入力値が使用されていないことが確認できたため、本診断は未実施としました。
@PostMapping("create")
public String create(@Validated({TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model, RedirectAttributes attributes) {
// ... (省略) ...
attributes.addFlashAttribute(ResultMessage.success("Created successfully!"));
return "redirect:/todo/list";
}
なお、Spring MVC でリダイレクトを扱う際は RedirectAttributes#addAttribute
メソッドを使用して URL を構築することで自動的に URI エンコーディングが行われるため、これらの脆弱性から保護することができます。
さいごに
Spring Boot と Spring Security を組み合わせて使用する場合、推奨される方法に従って実装していれば、多くの脆弱性から保護してくれます。とくに、今回検証したような簡単なアプリでは、開発者が手動で対策しなければならない箇所が非常に限られていることが確認できました。
本記事では取り上げなかったメール送信やファイル操作などの機能については機会を改めて検証し、まとめたいと思います。
それでは、最後までお読みいただき、ありがとうございました。
参考文献
▼ Macchinetta Server Framework (1.x) Development Guideline
▼ 安全なウェブサイトの作り方 | 情報セキュリティ | IPA 独立行政法人 情報処理推進機構