「またWAFのシグネチャ更新が必要か……」
深夜のアラート対応で、そうため息をついた経験を持つ方は多いのではないでしょうか。
Webアプリケーションへの攻撃は日々進化しています。SQLインジェクションやXSS(クロスサイトスクリプティング)といった古典的な攻撃でさえ、巧みにパターンを変えた「亜種」が登場し、従来のルールベース(正規表現マッチング)だけでは検知しきれないケースが増えています。あるいは、防御ルールを厳しくしすぎて、正規のユーザーまでブロックしてしまう「誤検知(False Positive)」に悩まされることも少なくないでしょう。
もし、システムが「攻撃の具体的なパターン」を知らなくても、「何かがおかしい」という予兆を統計的に感じ取れるとしたらどうでしょうか。
本記事では、Pythonと機械学習(Machine Learning)を用いて、Webアクセスログから悪意のあるリクエストをリアルタイムで識別する「自作検知エンジン」の構築方法を解説します。ブラックボックスになりがちな商用AI製品ではなく、中身を理解し、コントロールできる「透明なAI」を実装していくアプローチです。
もちろん、これが銀の弾丸というわけではありません。しかし、ルールベースの防御網をすり抜けてくる未知の脅威に対する「第二の矢」として、機械学習は非常に強力な武器になります。既存の業務フローやシステムに無理なく組み込める、現実的で運用しやすい解決策を探っていきましょう。
なぜ今、ルールベース検知に「機械学習」を重ねるべきなのか
多くのWebサービスでは、WAF(Web Application Firewall)を導入してセキュリティ対策を行っています。しかし、一般的なシグネチャ型(ルールベース)のWAFには構造的な限界があります。
シグネチャ型WAFが抱える「イタチごっこ」の構造的限界
ルールベースの検知は、基本的に「既知の攻撃パターン」との照合です。例えば、「SELECT * FROM」や「<script>」といった文字列が含まれていればブロックする、といった具合です。
しかし、攻撃者はこのルールを回避するために、文字コードを変えたり、空白をコメントアウト記号に置き換えたり(難読化)してきます。防御側が新しいルールを追加すれば、攻撃側はまた新しい回避策を編み出す。この「イタチごっこ」は終わりがありません。
さらに問題なのは、ゼロデイ攻撃です。まだ世に知られていない脆弱性を突く攻撃パターンは、当然ながらシグネチャリストには存在しません。つまり、最初の攻撃は「素通り」してしまうリスクが高いのです。
機械学習が「未知の悪意」を見抜ける統計的理由
ここで機械学習の出番です。機械学習、特に今回扱うアプローチでは、特定の文字列を探すのではなく、リクエスト全体の「特徴」を統計的に分析します。
例えば、正常なHTTPリクエストには以下のような統計的傾向があります。
- URLの長さは一定範囲に収まることが多い
- 特殊記号(
',",;,--など)の出現頻度は低い - パラメータの値はアルファベットと数字が中心
一方、SQLインジェクションやOSコマンドインジェクションを狙うリクエストは、異常に長かったり、記号の密度が高かったり、通常とは異なる文字の並び(n-gram)を含んでいたりします。
機械学習モデルは、これらの「傾向のズレ」を多次元空間上の距離や境界線として学習します。そのため、具体的な攻撃コードを知らなくても、「このリクエストは普段の正常な通信とは明らかに構造が異なる(=怪しい)」と判断できるのです。これが、未知の攻撃(Zero-day)や亜種に対してもある程度の検知力を発揮できる理由です。
本記事で作成する検知エンジンの全体アーキテクチャ
今回構築するのは、非常にシンプルかつ拡張性の高いパイプラインです。保守性や運用のしやすさを考慮し、既存のシステムにスムーズに統合できる設計を目指します。
- Input: Webサーバー(Nginx/Apache)のアクセスログ、またはアプリケーション層で受け取ったHTTPリクエスト。
- Feature Extraction: リクエスト文字列から「長さ」「記号数」「キーワード出現」などの数値を抽出。
- Inference (Model): 学習済みモデル(Random Forest等を想定)に入力し、正常(0)か攻撃(1)かを判定。
- Output: 判定結果と確信度(Probability)を返す。
このエンジンをマイクロサービスとしてAPI化しておけば、既存のシステムに大きな変更を加えることなく、「怪しいリクエストのスコアリング」機能を追加できます。
Step 1:学習データの準備と「特徴量エンジニアリング」の極意
機械学習プロジェクトの成否は、モデルのアルゴリズムよりも「データの質」と「特徴量の設計」で8割決まると言っても過言ではありません。セキュリティ分野も例外ではありません。
良質なデータセットの入手源
自社のログがあればベストですが、学習には「正常なログ」だけでなく「攻撃ログ」も大量に必要です。自社ログだけでは攻撃サンプルが不足しがちなので、まずは公開されているデータセットを利用してベースモデルを作ることをお勧めします。
代表的なものとして、CSIC 2010 HTTP Datasetがあります。これはWeb攻撃検知の研究で広く使われているデータセットで、正常なトラフィックと、SQLインジェクションやバッファオーバーフローなどの攻撃トラフィックが含まれています。
- CSIC 2010 HTTP Dataset: 学術研究用として標準的。
- HTTP DATASET (Kaggle): より手軽に試せるCSV形式のものも多数公開されています。
今回は、これらのような「リクエスト内容(URL, Body等)」と「ラベル(正常/攻撃)」がペアになったデータがある前提で進めます。
生のアクセスログを「機械が読める数値」に変換する
機械学習モデルは文字列をそのまま理解できません。すべてを「数値(ベクトル)」に変換する必要があります。この変換プロセスこそが、機械学習において重要な特徴量エンジニアリングです。
単純に単語をID化するだけでは不十分です。セキュリティ固有のドメイン知識を活かした特徴量を作りましょう。
以下のような特徴量が有効です:
統計的特徴:
- URLの長さ、パラメータの長さ
- 特殊記号(
<>,',",(,)など)の出現回数 - 大文字/小文字の比率
- 数字の比率
キーワード特徴:
- SQL予約語(
select,union,drop)の有無 - スクリプトタグ(
script,alert)の有無 - パストラバーサル(
../,etc/passwd)の有無
- SQL予約語(
攻撃パターンを浮き彫りにするn-gram解析とエントロピー計算
さらに踏み込んで、n-gramとエントロピーも活用しましょう。
- n-gram: 文字列をn文字ごとの切り出しとして扱います。例えば "admin" の2-gramは
ad,dm,mi,inです。攻撃コードには、自然言語にはあまり現れない特定の文字の並びが頻出します。 - エントロピー(情報量): 文字列の乱雑さを表します。暗号化されたペイロードや、難読化された攻撃コードは、通常の単語よりもランダム性が高く、エントロピー値が高くなる傾向があります。
では、Pythonでこれらの特徴量を抽出するクラスを書いてみましょう。
import pandas as pd
import numpy as np
import re
from urllib.parse import unquote
class SecurityFeatureExtractor:
def __init__(self):
# 注目する特殊文字
self.special_chars = ["<", ">", "'", "\"", ";", ":", "(", ")", "{", "}", "[", "]", "|", "*", "$", "^", "`", "~"]
# 注目するキーワード(簡易版)
self.suspicious_keywords = ["select", "union", "insert", "drop", "script", "alert", "eval", "passwd", "etc", "../"]
def _calculate_entropy(self, text):
if not text:
return 0
prob = [float(text.count(c)) / len(text) for c in dict.fromkeys(list(text))]
entropy = -sum([p * np.log2(p) for p in prob])
return entropy
def transform(self, request_list):
features = []
for req in request_list:
# URLデコード
decoded_req = unquote(req).lower()
feature = {}
# 基本統計量
feature['length'] = len(decoded_req)
feature['num_digits'] = sum(c.isdigit() for c in decoded_req)
feature['num_alphas'] = sum(c.isalpha() for c in decoded_req)
feature['ratio_special'] = sum(not c.isalnum() for c in decoded_req) / (len(decoded_req) + 1)
# 特殊文字カウント
for char in self.special_chars:
feature[f'count_{char}'] = decoded_req.count(char)
# キーワードカウント
for keyword in self.suspicious_keywords:
feature[f'has_{keyword}'] = 1 if keyword in decoded_req else 0
# エントロピー
feature['entropy'] = self._calculate_entropy(decoded_req)
features.append(feature)
return pd.DataFrame(features)
# 使用例
# extractor = SecurityFeatureExtractor()
# df_features = extractor.transform(["/index.php?id=1", "/login.php?user=admin' OR '1'='1"])
# print(df_features.head())
このコードを実行すると、生のURL文字列が、機械学習モデルが解釈可能な「数値の表(DataFrame)」に変換されます。これがモデルへの入力となります。
Step 2:検知モデルの選定とトレーニングの実践
データが準備できたら、次はモデルの学習フェーズに入ります。Webリクエストの検知において、アーキテクチャ選定で最も重視すべきは「精度」と「推論速度(レイテンシ)」のバランスです。実務に導入する上では、運用コストや保守性も重要な判断基準となります。
リアルタイム検知に向くアルゴリズム比較
AIによる検知と聞くと、まずDeep Learningを思い浮かべるかもしれません。確かに、Transformerや、近年LLM向けに再設計され注目を集めているxLSTM(eXtended LSTM) などのアーキテクチャを採用すれば、文脈を深く理解した高精度なモデルが構築可能です。
しかし、WAFのコンテキストでは以下の課題が残ります:
- 推論レイテンシ: 複雑なDeep Learningモデルは、リクエストごとに数十〜数百ミリ秒の遅延を生む可能性があります。
- リソースコスト: 高速な推論にはGPUが必要となるケースが多く、運用コストが跳ね上がります。
したがって、実務的な選択肢としては、CPUでも高速に動作する以下の「軽量な機械学習アルゴリズム」が適しています。
- Random Forest (ランダムフォレスト): 決定木を多数組み合わせるアンサンブル学習。精度が高く、どの特徴量が検知に寄与したか(解釈性)が把握しやすいため、セキュリティ運用と相性が良いです。
- Logistic Regression (ロジスティック回帰): 非常に高速でシンプル。解釈も容易で、ベースラインモデルとして優秀です。
- LightGBM / XGBoost: 勾配ブースティング決定木。Kaggle等のコンペティションでは定番ですが、パラメータ調整がやや複雑です。
今回は、過学習しにくく、デフォルト設定でも安定した精度が出やすいRandom Forestを採用します。最初のプロトタイプ構築には最適な選択です。
Scikit-learnを使ったモデル学習のコード実装
それでは、抽出した特徴量を使ってモデルを学習させましょう。Pythonのデファクトスタンダードである scikit-learn を使用します。
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
import joblib
# ダミーデータの生成(実際は前処理済みのデータをロードしてください)
# 0: 正常, 1: 攻撃
# df_features = ... (Step 1で生成したDataFrame)
# labels = ... (対応するラベルのリスト)
# ここでは解説用に仮の変数名を使用します
# X = df_features
# y = labels
# データを学習用(80%)とテスト用(20%)に分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# モデルの初期化と学習
# n_estimators=100: 決定木の数。多いほど安定するが計算コストが増える
# n_jobs=-1: 全CPUコアを使用して並列処理を行う
clf = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
clf.fit(X_train, y_train)
# テストデータでの評価
y_pred = clf.predict(X_test)
print("=== Classification Report ===")
print(classification_report(y_test, y_pred))
# モデルの保存(実運用で使用するため)
joblib.dump(clf, 'waf_model_rf.pkl')
print("Model saved to waf_model_rf.pkl")
このスクリプトを実行すると classification_report が出力されます。セキュリティの観点で最も注目すべき指標は、攻撃クラス(通常は 1)に対する Recall(再現率) です。これは「実際の攻撃のうち、どれだけを見逃さずに検知できたか」を示しており、WAFにおいては誤検知(False Positive)よりも見逃し(False Negative)のリスク評価に関わる重要な数値です。
不均衡データ(攻撃が少ない)への対処法
実際のWebアクセスログでは、正常な通信が99%以上を占め、攻撃通信はごくわずかという「不均衡データ(Imbalanced Data)」の状態が一般的です。このまま学習させると、モデルは「すべて正常と判定すれば99%正解できる」という安易な解を学習してしまい、攻撃を全く検知できないモデルになってしまいます。
この課題には、以下の対策が有効です。
- Class Weightの調整:
RandomForestClassifier(class_weight='balanced')と設定することで、出現頻度の少ないクラス(攻撃)の重みを自動的に大きくし、ペナルティを強化します。 - SMOTE等のオーバーサンプリング:
imbalanced-learnライブラリなどを使用し、少ない攻撃データを人工的に合成して増やし、データセットのバランスを取る手法です。
まずは実装が容易な class_weight='balanced' を設定し、それでもRecallが低い場合にSMOTEの導入を検討するのが効率的なアプローチです。
Step 3:推論エンジンのAPI化とリアルタイム判定パイプライン
モデルができたら、それをWebサーバーから呼び出せる形にします。Pythonなら FastAPI を使うのが現代的な選択です。Flaskよりも高速で、非同期処理にも強いため、高負荷な環境に適しています。
Flask/FastAPIによる推論APIの構築
以下は、FastAPIを使った最小限の推論サーバーの例です。
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import joblib
import pandas as pd
# 先ほどのSecurityFeatureExtractorクラスもインポートしておく
app = FastAPI()
# モデルと特徴量抽出器のロード(起動時に一度だけ実行)
model = joblib.load('waf_model_rf.pkl')
extractor = SecurityFeatureExtractor()
class LogRequest(BaseModel):
path: str # リクエストパス
body: str = "" # リクエストボディ(あれば)
@app.post("/predict")
def predict(req: LogRequest):
try:
# 文字列を結合して解析対象とする
target_text = req.path + " " + req.body
# 特徴量抽出
df = extractor.transform([target_text])
# 推論
prediction = model.predict(df)[0]
probability = model.predict_proba(df)[0][1] # 攻撃である確率
return {
"is_attack": bool(prediction),
"confidence": float(probability),
"message": "Block" if prediction else "Allow"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# 実行コマンド: uvicorn main:app --reload
レイテンシを最小化する前処理の最適化
このAPIをNginxの auth_request モジュールなどで呼び出すことで、リクエストがアプリケーションに到達する前に検査を行うことができます。
注意点として、特徴量抽出の処理(特に正規表現やループ処理)はCPUを使います。Pythonはスクリプト言語なので、文字列処理がボトルネックになりがちです。本番環境では、特徴量抽出部分をCythonで高速化したり、Rustで書き直してPythonから呼び出すなどの最適化が必要になる場合があります。まずはPythonのみで実装し、レイテンシ(応答時間)を計測してからチューニングを行いましょう。
Step 4:誤検知(False Positive)との戦いと運用チューニング
システムを稼働させると、必ず直面するのが「誤検知」です。正当なユーザーが「攻撃だ」と判定されてブロックされてしまうと、ビジネス機会の損失やクレームにつながります。
「正常を攻撃と判定する」リスクの許容値設定
機械学習モデルは「0か1か」だけでなく、「確率(Probability)」を出力します(先ほどのコードの confidence)。
- 確率 90%以上 → 即ブロック
- 確率 70%〜90% → ブロックはせず、管理者へのアラート通知のみ(監視モード)
- 確率 70%以下 → 通過
このように、閾値(Threshold)を設けて運用するのが鉄則です。最初は閾値を高め(緩め)に設定して「監視モード」で運用し、ログを見ながら徐々に閾値を調整していくのが安全な導入ステップです。
人間によるフィードバックループ(Human-in-the-loop)の設計
AIは運用しながら賢くしていくものです。誤検知が発生したら、そのログを収集し、正しいラベル(「これは正常でした」)を付けて再学習データセットに追加します。
このHuman-in-the-loop(人間参加型)のサイクルを作ることが、検知エンジンの寿命を延ばす鍵です。例えば、社内管理画面から「誤検知報告ボタン」を押すと、そのログが自動的に次回の学習データプールに蓄積されるような仕組みを作っておくと、運用が非常に楽になります。現場の業務フローに自然に組み込める仕組みづくりが、AI活用の成功を左右します。
まとめ:自作エンジンを既存セキュリティスタックに統合する
ここまで、Pythonを使った自作WAF検知エンジンの作り方を解説してきました。アクセスログを数値化し、機械学習モデルに判断させるプロセスは、一見難しそうですが、Scikit-learnを使えば数十行のコードで実装可能です。
多層防御の一環としての位置づけ
重要なのは、これを既存のセキュリティ対策とどう組み合わせるかです。
- 第1層(WAF製品): 明らかな攻撃(既知のシグネチャ)はここで高速に弾く。
- 第2層(自作MLエンジン): WAFをすり抜けた怪しいリクエストを、今回のエンジンで詳細に検査する。
- 第3層(アプリロジック): 最終的なバリデーション。
自作エンジンを第2層に配置することで、商用WAFの隙間を埋める強力なセーフティネットとなります。また、自社特有の攻撃パターン(例えば、ゲームのチート行為や、ECサイトの転売botなど)に特化して学習させることで、汎用製品にはできない防御が可能になります。
次のステップ:深層学習(Deep Learning)への拡張可能性
今回は軽量なRandom Forestを使いましたが、データ量が数百万件を超えてくるなら、LSTMやTransformer(BERTなど)を用いたDeep Learningモデルへの移行も視野に入ります。これらは文字の並び順(シーケンス)をより深く理解できるため、難読化された複雑な攻撃の検知精度がさらに向上します。
まずは手元のログデータを集め、今回紹介したコードを動かしてみてください。自ら構築したAIモデルが、未知の攻撃を的確に捉えるプロセスを体感できるはずです。
セキュリティは「守り」ですが、エンジニアリングとしては非常にクリエイティブな領域です。データ分析と機械学習の力を活用し、システムをより堅牢で運用しやすいものへと進化させていきましょう。
コメント