スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
detection pipeline

検出しても、画面に出なかった
絞り込みが、新しい検出を捨てる

逆引き検証を実装して、偽Googlebot を見つけました。cron のログには警告が出ています。 ところがダッシュボードの遮断候補には表示されません。 検出は成功し、表示は失敗している。その間に何があったのか—— 4段階すべてで落ちていたという話です。

検知パイプライン MongoDB 集約 スコアリング

[ § 0 ] 導入

見つけたのに、見えなかった

前回、UAは自己申告にすぎないという話を書きました。 Googlebot と名乗るだけでホワイトリストを通過してしまうので、 逆引き→正引きの往復確認を実装した。

cron のログに、こう出ました。

/var/log/apache-agg.log
[WARN] クローラ詐称の疑い: 108.165.181.162
       PTR=108-165-181-162.ips.acedatacenter.com
       (既知クローラのドメインではない)
       UA=Mozilla/5.0 (compatible; Googlebot/2.1; ...)

検出できています。ところがダッシュボードの遮断候補を開くと、そのIPがありません。 エラーも出ていない。ただ、無いのです。

「検出できた」と「画面に出た」は、別のことでした。

[ § 1 ] 構造

経路を、分解する

検出から画面までは一続きではありません。データは4つの段階を通ります。

段階やること落とす基準
① 検出cron が逆引きで確かめる
② 保持IP別の指標を上位60件に絞って保存404+403 の多い順
③ 取得API が集約して上位N件を返す同上
④ 表示スコアで足切り45点以上

②③④はすべて「絞り込み」です。 IPは無数にあるので、全部を保存・送信・表示するわけにはいきません。 データ量を抑えるために入れたもので、それ自体は妥当な判断でした。

問題は、絞り込みの基準です。 どれも「404の多い順」で並べていました。 攻撃者を探すには自然な物差しです。ですが—— 詐称は、404が0件でも成立します
[ § 2 ] 実測

4段すべてで、落ちていた

問題のIPの数字は、こうでした。

項目
4040 件
4030 件
総アクセス3 件
攻撃パス0 種
スコア(詐称 +40 のみ)40 点

静かなのです。404を1件も出していない。 Googlebot を名乗って、数回アクセスしただけ。 攻撃者としては極めて地味で、だからこそ従来の物差しでは拾えませんでした。

段階この相手の値結果
① 検出逆引きが往復しない✓ 通過
② 保持404+403 = 0件 → 最下位✗ 上位60件から脱落
③ 取得クローラが枠を占有✗ 押し出される
④ 表示40点 < 45点5点足りない

①だけが正しく動いていました。 検出ロジックには一切の問題がなく、 その後ろで3回捨てられていたのです。

[ § 3 ] 経緯

なぜ4回も見落としたか

一度で直せませんでした。直すたびに「直った」と思ったのが問題でした。

1回目:スコアの閾値

40点では45点に届かない。詐称は点数によらず表示するようにしました。 1時間ビューで確認すると、確かに出ました。直った、と思いました。

2回目:24時間ビューで消えた

期間を広げたら、また見えない。1時間ではIP数が少なく上位に残っていただけで、 24時間では絞り込みで切られていたのです。

3回目:7日ビューで通常の候補が0件に

クローラを優先的に残すよう並べ替えを変えたら、今度は逆の事故が起きました。 クローラが枠を占有し、本来の遮断候補が全部押し出されて0件になった。 期間を広げたのに候補が減るという、あり得ない表示です。

4回目:古いデータには新しい印が無い

「クローラを名乗った」という印(フラグ)を付けて優先保持する仕組みを入れましたが、 それ以前に記録されたデータには、その印がありません。 過去分は相変わらず沈んだままでした。

段階ごとに、原因が違いました。 閾値、並べ替え、枠の共有、データの世代—— 1つ直すと、次の段階で詰まっているものが現れる。 「検出できた」と「画面に出た」の間には、これだけの段差がありました。
[ § 4 ] 修正

各段を、どう直したか

④ 閾値 — 決定的な証拠を、加点に混ぜない

スコアの加点方式は、弱い証拠を積み上げるための仕組みです。 404が多い、成功率が低い、攻撃パスを叩く——どれも単独では決め手にならないので、足し合わせる。

ところが詐称は単独で決定的です。積み上げる必要がない。 それを40点という数字にして加点の枠組みに入れた時点で、 閾値と競わせてしまったのが誤りでした。

log-api.php
// 詐称は点数によらず必ず表示する
if ($excl === null && !$spoofed && $score < 45) continue;
//                   ^^^^^^^^^ これを足した

②③ 絞り込み — 目的の違うものを、同じ枠で競わせない

「404の多い順」は攻撃者を探す並べ方です。詐称は404が0でも成立するので、 同じ物差しでは必ず沈みます。集約を2本に分け、種類ごとに枠を確保しました。

種類別に切り出す
$picked = array_merge(
    array_slice($bySort['spoof'],    0, $limit),
    array_slice($bySort['normal'],   0, $limit),
    array_slice($bySort['excluded'], 0, $limit)
);

② 保持 — 順位に、運を任せない

優先フラグを付ける方式は、過去のデータには効きません。 そこで発想を変えました。検証結果は既に別のコレクションに記録されているのだから、 詐称と判明しているIPは名指しで取りに行けばいい。 並べ替えの結果に依存する必要はありませんでした。

「確定した事実は、順位で扱わない」—— これが今回いちばんの学びでした。 スコアも並べ替えも上位N件も、不確かなものを優先度で捌くための道具です。 確定しているものを、その仕組みに乗せてはいけませんでした。
[ § 5 ] 設計

同じデータを、違う物差しで扱っていた

根っこにあった間違いは、これだと思います。

何を見ていたか
検出名乗りと実体の不一致(件数は無関係)
保持・取得・表示件数の多さ

検出は「質」で判定し、それ以降は「量」で並べていました。 量が少ない質の高い検出は、経路の途中で必ず失われます

そして絞り込みの基準は、たいてい後から足したものです。 データ量が増えてきたから上位60件に、応答が重いから150件に—— 性能のために入れた判断が、後から追加した検出と噛み合わなくなる。 足したときには存在しなかった検出のことを、絞り込みは知りません。

[ § 6 ] 体感デモ

検出は、どこで消えたか

3種類の相手を、4段階のパイプラインに流します。 どこで落ちるかを見てください。

Interactive · detection pipeline
4段階を、通り抜けられるか
実際に観測した値を用いています。修正前/修正後で挙動が変わります。

偽Googlebot は、修正前だと②で止まります。 そこから先の段階は、そもそも到達すらしていません。

注目してほしいのは「通常のスキャナ」です。 404が1870件、攻撃パス12種、スコア100点——この相手は 修正前でも4段すべてを通過します

つまり、よくある攻撃者でテストしていたら、この欠陥には気づけません。 パイプラインは「正常に動いている」ように見えます。 新しい検出を足したら、その検出が通る相手でテストしなければ意味がない—— 当たり前のようでいて、実際には既存のテストデータをそのまま使ってしまいます。
[ § 7 ] まとめ

検出は、経路の入口にすぎない

今回、検出ロジックには最初から問題がありませんでした。 逆引きの往復確認は正しく実装され、正しく詐称を見つけていた。 それでも4日間、画面には何も出ませんでした

そして怖いのは、例外もエラーも出ないことです。 cron は正常終了し、APIは200を返し、画面は正常に描画される。 ただ、そこに無いだけ。

この記事のまとめ

  • 検出から表示までは4段階ある。検出・保持・取得・表示。
  • 後ろ3つはすべて絞り込み。何かを捨てる判断が埋まっている。
  • 絞り込みの基準は「件数の多さ」だった。詐称は404が0でも成立するので沈む。
  • 確定した事実を、優先度の仕組みに乗せない。スコアも順位も、不確かなものを捌く道具。
  • 優先フラグは過去のデータには効かない。既にある事実は名指しで取りに行く。
  • よくある攻撃者でテストすると気づけない。新しい検出には、それが通る相手を用意する。
  • 例外もエラーも出ない。画面に出るところまで確認して、初めて完了

関連記事

『検出しても、画面に出なかった — 絞り込みが、新しい検出を捨てる』を公開しました。