脆弱性が診断ツールで見つかったり見つからなかったりする話
はじめに
「脆弱性診断内製化ガイド」が注目を集めていたことをきっかけに、「診断ツールで見つけられそうな脆弱性が見つかったり見つからなかったりする」話を書けないかな?と思い立ちました。本記事では、診断ツールを用いて簡単なアプリケーションをスキャンした結果を紹介します。
背景
脆弱性診断内製化ガイド
2025年7月31日、情報処理推進機構(IPA)から「脆弱性診断内製化ガイド」というドキュメントが公開されました。
これは「中核人材育成プログラム 卒業プロジェクト」によるもので、企業が自社内で脆弱性診断を行う「内製化」を推し進めるためのガイドです。内製化に必要な組織体制・人材育成、スモールスタートから始める導入ステップ、外部委託と比べたメリット・デメリットなどが述べられています。
診断ツールによる脆弱性の検出
一方、SNSでは「あるページ」に注目が集まっているように感じました。それは、P.51から始まる「付録 A:技術検証結果について」。その中でも特にP.56の「A.4 BadTodo に対する Web アプリケーション診断」に関する言及を複数見かけました。「BadTodo」に対して5種類のツールで診断を行ったところ、多くの脆弱性を検出できなかった‥‥という結果が示され、手動診断による補完やツールの特性を理解することの重要性が説かれています。
「BadTodo」は、「徳丸本」こと「体系的に学ぶ 安全なWebアプリケーションの作り方」で著名な徳丸浩氏が公開する、意図的に多くの脆弱性を埋め込んだ「脆弱性診断実習用アプリ」です。
ただ、ひとことで「診断ツール」といっても、「URLを指定してスキャンボタンを押すだけ」の手軽なものから、「診断したいHTTPリクエストをあらかじめ特定しておき、複数のHTTPリクエストの実行順序(≒画面遷移)も指定してからスキャンする」といった具合に前準備が必要なものなど様々です。たとえば前者の場合、脆弱性を見つける以前の問題として、そもそも診断したい画面・機能までツールが到達しているのかも確認しないと分かりません。ひとつの製品で複数の使い方が出来ることも珍しくありませんし、設定もたくさんあります。
その点に関して、ガイドの技術検証結果からは、どのツールをどのように使ったのかは不明でした。それでも、「ツールを導入しただけでは脆弱性を十分に検出できないかもしれない」ことが「脆弱性診断内製化」のためのドキュメントで分かりやすくまとめられたことは大切だと思います。
前置きが長くなりました。今回のガイドの内容をうけて、ツールで見つけられそうな脆弱性が見つかったり見つからなかったりする例を、簡単なアプリケーションでの動作を踏まえて見ていきます。
準備
診断ツールの用意
今回は「ZAP」を使います。検証時点で最新のv.2.16.1、Windows版を利用しました。
「ZAP」は以前「OWASP ZAP」として知られていた歴史の長い診断ツール。2023年にOWASPから離れ、現在は「ZAP by Checkmarx」として提供されています。Webアプリケーションを対象としたツールのうち、無償で使えるものの筆頭にあがる1つでしょう。Docker版も提供されており、CI/CDパイプラインへのDAST組み込みの用途でも見聞きします。
検証用アプリケーションの用意
ZAPを実行する対象として、SQLインジェクションの脆弱性がある簡単なアプリケーションを用意しました。というか、Claudeにお願いして、脆弱性のある状態のコードを書いてもらいました。「開始年」「終了年」「キーワード」を入力すると検索結果が返ってくるだけのフォームです。Python Flask+SQLiteで実装されています。
ソースコードの抜粋です。HTTPリクエストで受け取った入力値をチェックして、SQL文を実行します。SQL文には利用者の入力値が直接埋め込まれており、脆弱性がありそうな雰囲気です。
# Validate required fields
if not start_year or not end_year or not keyword:
error_message = "すべての項目を入力してください。"
elif len(start_year) > 4:
error_message = "開始年は4文字以内で入力してください。"
elif len(end_year) > 4:
error_message = "終了年は4文字以内で入力してください。"
elif not start_year.isdigit():
error_message = "開始年は数字のみで入力してください。"
elif not end_year.isdigit():
error_message = "終了年は数字のみで入力してください。"
else:
search_performed = True
try:
# VULNERABLE SQL CONSTRUCTION - FOR DEMONSTRATION PURPOSES ONLY!
# This is intentionally vulnerable to SQL injection
query = f"""
SELECT id, year, title, content
FROM news
WHERE year >= {start_year}
AND year <= {end_year}
AND (title LIKE '%{keyword}%' OR content LIKE '%{keyword}%')
ORDER BY year DESC, id DESC
"""
print(f"Executing SQL: {query}") # For demonstration/debugging
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(query)
results = cursor.fetchall()
conn.close()
except Exception as e:
error_message = f"検索エラーが発生しました: {str(e)}"
IPA「安全なウェブサイトの作り方」の別冊、「ウェブ健康診断仕様」でも触れられている典型的な検査文字列を検索キーワードに付け加えてみると、構文エラーが発生したことが分かります。これに加え、複数のパターンを試して、アプリケーションにSQLインジェクション脆弱性があることを確認しました。
ツールでスキャンしてみよう
ツールでスキャンしてみよう Part.1
さて。準備が整ったところでZAPでこのアプリケーションを動的スキャンしてみましょう。
レスポンスヘッダなどから判明するアラート8種類が静的スキャンにより既に見つかっている状態から開始していきます。Part.1では特段の設定変更をせずに動的スキャンしてみます。
👆前提として、もちろん「ZAP」はSQLインジェクションの検出に対応しています。動的スキャンのポリシーでは、SQLインジェクション関連のテストが特定のDBMSに特化したものも含め複数種類用意されています。すべて「既定」のまま「スキャンを開始」してしばらく待機します。
👆画面中ほど右の方に「New Alerts: 12」と表示されました。何が検出されたのかは「アラート」タブで確認します。
👆新しいアラート12件は、すべて「User Agent Fuzzer」というリスクレベル Information の検出によるものでした。SQLインジェクションは‥‥見つかっていないようです。
ツールでスキャンしてみよう Part.2
Part.1では設定を特に変更せずにスキャンしてみました。Part.2では多くの脆弱性を見つけられるように設定を変えてみましょう。
ZAPのポリシーでは、テスト名ひとつひとつの「しきい値(Threshold)」と「強度(Strength)」を調整できます。「しきい値」は潜在的な脆弱性を報告する度合いを制御するもの。低くすると、疑わしいかも?レベルのものまで脆弱性として検出する可能性が高まります。「強度」は実行する検査のパターンを制御するもの。高くすると、検査のパターンが増えます。
スキャンポリシーの説明は公式ドキュメントも参照ください。
👆ZAPをリセットし、Part.1の最初と同じ状態に揃えました。アプリケーションがDBMSとしてSQLiteを使っていることを既に把握しているため、「SQL Injection - SQLite」の設定のみ変更しました。一番極端な「しきい値:低」「強度:超」を選び、「スキャンを開始」してしばらく待機します。
👆今度は右の方に「New Alerts: 13」と表示されました。さっきより1件多いですね。何が検出されたのか「アラート」タブを確認します。
👆「SQL Injection - SQLite」の検出事項が1件現れています。パラメータ名「keyword」で、応答時間の差異によってSQLインジェクションが検出されたことが示されています。
Part.1とPart.2で検出事項に差が出ました。では、何でもかんでも「しきい値」を下げ、「強度」を上げればよいかというと、一概にそうともいえません。「しきい値」を下げるとそれだけ誤検出の可能性が高まり、検出結果が正しいか?本当に脆弱性なのか?の確認に時間を取られることになります。また、「強度」を上げるとスキャンの時間が増大します。今回の対象アプリケーションは単純な検索機能のみでパラメータも少ないため時間は気になりませんでしたが、機能・画面やHTTPリクエスト内のパラメータの数が増えるにつれて、スキャンに要する時間も長くなっていきます。
ツールでスキャンしてみよう Part.3
期待した脆弱性が見つかって一安心したところで、もう1つお付き合いください。
Part.2でSQLインジェクションがどう検出されたのか、スキャンで使われた文字列を眺めてみましょう。なんだか長いですよね。数えると68文字でした。部分一致検索するフォームで、何十文字も入力できる必要は無いかもしれません。ためしに、全角半角の区別なく50文字を上限にしてみましょう。
elif len(keyword) > 50:
error_message = "キーワードは50文字以内で入力してください。"
👆冒頭のソースコードの入力値チェックに条件を1つだけ足してみました。
👆ZAPをリセットし、もう一度はじめから動的スキャンします。Part.2と同様「SQL Injection - SQLite」だけ「しきい値:低」、「強度:超」とし、「スキャンを開始」して待機します。
👆「New Alerts: 12」になっています。「アラート」タブを確認します。
👆SQLインジェクションがまた見つからなくなってしまいました。
入力制限を設けた‥‥といっても、今回追加した「入力値50文字まで」はそう非現実的な厳しい制限ではないでしょう。これだけで脆弱性が見つからなくなりました。もちろん、SQLインジェクション脆弱性が無くなったわけでないことは言うまでもありません。Part.2で脆弱性を検出してくれた文字列が「入力値50文字まで」というちょっとした制限で弾かれ、脆弱性なしと判定されてしまいました。
結果の総括
少しずつ設定や環境を変えた3つの例をもとに、診断ツールによる検出結果を確認しました。
Part.1とPart.2の差異は「ツールの特性の理解」というには少し大げさで、検査のポリシーを変えただけです。ただ、このような違いだけで結果は変わりますし、ZAPには他にも細かな設定が豊富に用意されています。さらにはアドオンによる機能追加や独自ルールの追加もでき、同じツールを使っていたとしても「ツールの特性の理解」で検出結果も効率も大きく変わってきます。
Part.2とPart.3のように、本当にちょっとしたアプリケーションの仕様がツールの妨げになることもあります。今回のような分かりやすそうな脆弱性であっても、です。このアプリケーションを最初から「入力値50文字まで」で実装していたら、「スキャン完了!脆弱性ないね!」と安心してしまうかもしれません。
おわりに
本記事では、診断ツールによる検出結果について例を交えて紹介しました。「ツールで診断しているから大丈夫だ」という言い分を昔どこかで耳にしたことがあるのですが、「脆弱性診断内製化ガイド」でも示されているように、その考え方には注意が必要だと言えるでしょう。
もちろん、ツール診断が無意味だというメッセージでは決してありません。ツール診断と手動診断、そして、診断の内製化と外部委託。どれもメリット・デメリットがあり、そのバランスを取ることこそが重要である、と今回のガイドを読んで改めて感じました。気になった方は、IPAのサイトでぜひ読んでみてください。
Discussion