UAは Mozilla。でも、明らかにおかしい
ある日、ログでこんな1件を見ました。User-Agent はごく普通の Mozilla/5.0。
なのに、その日いちばんアクセスが多いIP。人間なら、そんなに読めません。
「名乗り」は正常に偽装できます。ならば、何で見分けるか。答えは振る舞い(パターン)です。 前回「正常な訪問者」を掴みました。今度はその裏——正常から外れるものを掴みます。
203.0.113.x / 198.51.100.x / 192.0.2.x)で、実在のアクセス元ではありません。
正常を知れば、異常が浮かぶ
これは TensorFlow.js の異常検知とまったく同じ構造です。 AutoEncoder が「正常を学べば、異常は再構成できずに浮かぶ」ように、 正常なアクセスの姿を基準にすれば、逸脱が測れます。
正常なアクセスとは、前回のセッション像そのもの。人間はページを読み、 適度に間隔をあけ、実在するページを辿ります。 攻撃者は、そのどれからも外れます。この逸脱を、4つの観点に分けて測ります。
同一IPの高頻度 — 速すぎる
人はページを読むのに時間がかかります。秒間に何十リクエストも投げるのは、人間業ではありません。
同一 addr の単位時間あたりアクセス数を数えれば、これは一発で浮かびます。
const count = log.filter(e => e.addr === ip && now - e.t < 60_000).length; if (count > 30) score.freq = 高; // 30req/60sec 超は要注意
前回のセッション化があるから、「1人あたり」の頻度がきちんと測れる、という土台も効いています。
同一UAの複数IP分散 — 散らばりすぎ
1つのIPを弾かれないよう、同じツール(同じ uah)が多数のIPに散ることがあります。
1IPあたりの頻度は低く抑えられていても、UAを軸に横断集計すると、不自然な分散が見えます。
const ips = new Set( log.filter(e => e.uah === ua).map(e => e.addr) ); if (ips.size > threshold) score.dist = 高;
uah と addr を1ドキュメントに持たせたから、この交差集計ができます。
「後から足せる器」が、こういう分析の自由度を生みます。
多ページ走査 & 存在しないパス — 探っている
正常な訪問は、実在するページを辿ります。攻撃者は違う。当たりを探して、
/wp-admin・/.env・/phpmyadmin のような
一般によく狙われるパスを総当たりします。これらはこのサイトに存在しないので、
404 への集中それ自体が、強いシグナルになります。
const reqs = log.filter(e => e.addr === ip); const rate404 = reqs.filter(e => e.status === 404).length / reqs.length; // 「読む」ではなく「探る」= 実在しないパスへの集中
名乗りと振る舞いの矛盾 — UAは自己申告
導入の1件がこれでした。Mozilla/5.0 は誰でも名乗れます。だから、名乗りと行動のズレを突きます。
ブラウザを名乗るなら、普通は CSS も JS も画像も読む。ところが攻撃ツールは、 HTML だけを非人間的な速度で舐めて、資産(css/js/画像)を一切読まない。robots も無視する。 名乗りと振る舞いが噛み合いません。
const claimsBrowser = /Mozilla|Chrome|Safari/.test(ua); const loadsAssets = reqs.some(e => /\.(css|js|png|jpg)$/.test(e.uri)); if (claimsBrowser && !loadsAssets) score.mismatch = 高; // ブラウザを名乗るのに資産を読まない
ログから、攻撃者を炙り出す
正常な訪問者と正規クローラに、数種類の攻撃者を混ぜてあります。「解析する」で4観点のスコアを算出し、 感度スライダーで閾値を動かしてください。攻撃者だけが複数観点で赤くなり、正常な訪問者は白のままです。
感度を上げすぎる(閾値を下げる)と、正規クローラや人間まで誤検出します。 下げすぎる(閾値を上げる)と、攻撃者を見逃します。 その中間に、攻撃者だけをきれいに拾う帯がある——単一観点ではなく、複数観点を重ねているからこそ、この帯が広く取れます。
検出の、その先 — 遮断は別レイヤー
ここまでは「見つける」話でした。実際に「止める」のは、別の仕事です。 検出 ≠ 遮断。この2つを分けて考えるのが肝心です。
検出した攻撃者候補を実際に遮断するのは、サーバー側の役目。
.htaccess による IPv4 / IPv6 / UA ブロック、サブネット単位の遮断、APCu レート制限——
これらの遮断の実装は、appw.jp の運用記事に譲ります。
cancelled_sessions が実際に降りたのと、同じ設計思想です。
名乗りでは隠れられる。振る舞いでは浮かぶ
攻撃者は User-Agent を偽装して名乗りでは隠れられますが、振る舞い(パターン)では浮かびます。 高頻度・分散・走査/存在しないパス・名乗りとの矛盾。単独でなく、スコアで重ねることで、 正常な訪問者や正規クローラを巻き込まずに攻撃者を拾えます。
- なぜログを MongoDB に
器:後から育つデータにスキーマレスが効く - PVからセッションへ
正常:uqid を束ねて訪問者の姿を掴む - 攻撃者は、パターンで浮かぶ(この記事)
異常:正常からの逸脱で攻撃者を検出
器(MongoDB)→ 正常(セッション)→ 異常(攻撃者検出)。3段で、5サイト共通ログ基盤の「観測」側が完成しました。 検出したその先、実際に「止める」実装は、appw.jp へ続きます。
この記事のまとめ
- UA(名乗り)は偽装できる。攻撃者は振る舞い(パターン)で見分ける。
- 4観点:①高頻度 ②同一UAの分散 ③走査/404 ④名乗りとの矛盾。
- 「正常を知れば異常が浮かぶ」——TF.js の異常検知と同じ発想。
- 単一観点は誤検出しうる。複数観点をスコアで重ねて確度を上げる。
- 検出 ≠ 遮断。止める実装(.htaccess・APCu)は appw.jp の別レイヤー。