偽通販検出サービス「詐欺ダメ.com」の Chrome 拡張に、サポート詐欺(画面が突然「ウイルスに感染しました、この番号に電話を」と迫るあれ)対応を組み込んだ。
設計判断がとても面白かったので実装の要点をまとめる。
この記事の要点(3 行)
- 今回解析した13検体では、多くが Google 広告経由でのみ発火するタイプだった。
Yahoo ニュースと言えども、危険と隣り合わせ - ブラックリストでは守れない。Chrome 拡張で「ページごと警告画面に差し替える」 方式に振り切った
- 設計の一貫した優先順位は 「敵だけを止める」より「正規サイトを壊さない」。
詐欺ダメ.com 本体で偽通販を毎日解析してきた誤検知対策のノウハウがそのまま効いた。
(検知条件の細部は、回避の手引きになるため意図的にぼかす)
検体の出どころ:ほぼ全部 Google 広告経由
今回は検体を 13 本 集めて解析した。多くに共通していたのは、
- 直接 URL アクセスすると 無害な「偽の会社紹介ページ」 が表示される
- 広告経由の追跡 ID を付けて遷移した時だけ、同じ URL がサポート詐欺画面に切り替わる
という挙動だ。つまり「怪しいサイトを見たから引っかかった」ではない。
Google 広告経由で配信される広告は、掲載先の信頼性だけでは安全性を判断できない
という実態が今回の検体からは見えてきた。訪問者側の注意で避けきれる話ではない。
きっかけも個人的な事情だった。
6 月に自分自身がこの手の広告を踏み、Google に通報したが広告アカウントは停止されないまま。いまも同種広告が大量出稿されている。
詐欺ダメ.com として拾うべきと判断した。
なぜブラックリストでは守れないのか
偽通販は「買わなければ被害は出ない」ので、危険 URL を配って回れば後続は守れる。
サポート詐欺は開いた瞬間に画面が奪われ、恫喝音声が鳴り、電話番号が覆い被さる。
リスト反映を待っている間、訪問者全員が被害候補になる。
| 偽通販 | サポート詐欺 | |
|---|---|---|
| 被害発生 | 商品を注文した時 | ページを開いた瞬間 |
| 気付いてから守れる時間 | 数分〜数日 | ゼロ |
| ブラックリストで後続を守れるか | 守れる | 守れない(全員が被害者候補) |
事前情報に頼らず、初回訪問の時点で守る設計が必要だ。
設計思想:致命傷だけ守る、味方を巻き込まない
「何をやらないか」を先に決めた。
| 種別 | 具体例 | 対応方針 |
|---|---|---|
| 致命的な被害 | 電話をかける・遠隔操作ツールを入れる・ギフト券番号を伝える・クレカ入力 | 止める |
| 一時的な被害 | 全画面奪取・キーボード無効化・音声恫喝 | 完全に止めなくてよい(最悪、PCの電源長押しで解決) |
一時的な被害の完全阻止を目指すと副作用(動画サイトの全画面が壊れる、Gmail の下書き警告が消えるなど)と切りがない。
目的を 致命傷の防止に絞ったことで、副作用を極力避ける設計が成立した。
この線引きは、偽通販検出で「誤検知は 1 件でも信用を失う」と学んできた
詐欺ダメ.com 本体側のノウハウから来ている。
実装:Chrome 拡張で「ページごと差し替える」
サポート詐欺のページは、全画面表示・マウスカーソル奪取・音声再生・キーボード無効化・離脱妨害など、多数の仕組みを一斉に握って離さない。
これを一つずつ「解除」する実装は、何度も副作用で撤回してきた。
採用したのは、タブを警告ページに差し替えて元ドキュメントごと破棄する方式。
[ページ内 hook] 危険な API 呼び出しを検知
↓ postMessage
[ISOLATED world bridge] 拡張の裏側に転送する橋渡し
↓ chrome.runtime.sendMessage
[background] chrome.tabs.update(tabId, {url: warning.html})
↓
元ページと、握られていた仕組み全部が消滅・警告表示
これは Chrome 拡張だからこそ実現できた 割り切りだ。
JS の実行と DOM を、拡張が同じレイヤーで観測・介入できるからこそ、「タブを差し替える」の一言で全部を巻き添えにできる。
3 つの実行コンテキストをまたぐ処理だが、流れ自体はシンプルだ。
// MAIN world hook(ページ本体と同じ空間)
if (isMalicious(args)) {
window.postMessage({
type: 'sagidame',
action: 'kill',
id: 'P1-XX'
}, '*');
throw new DOMException('blocked', 'NotAllowedError');
}
// ISOLATED world bridge(拡張だけの独立空間)
window.addEventListener('message', (e) => {
if (
e.source !== window ||
e.data?.type !== 'sagidame' ||
e.data?.action !== 'kill'
) return;
chrome.runtime.sendMessage({
kill: true,
id: e.data.id
});
});
// background service worker
chrome.runtime.onMessage.addListener((msg, sender) => {
if (!msg?.kill || !sender.tab) return;
chrome.tabs.update(sender.tab.id, {
url: chrome.runtime.getURL(`warning.html?id=${msg.id}`),
});
});
三者はそれぞれ違う空間で動くが、postMessage と chrome.runtime.sendMessage の
2 段バケツリレーで結ばれている。
なぜ「MAIN world」と「ISOLATED world」を分けるのか
Chrome 拡張のスクリプトは既定では ページ本体とは別の隔離空間 で動く。
安全な設計だが、window.atob を横取りするような hook はここからでは効かない。
そこで ページ本体と同じ空間で走るスクリプト(MAIN world) を明示的に用意する。
一方 MAIN world は拡張の裏側と通信できないので、隔離空間側にもう 1 本置いて橋渡しさせる。
今回の用途では、この 2 段構成が扱いやすかった。
3 層構成
差し替えを走らせる判定は 3 層に分けた。
単一ルールでは誤検知が増え、逆にすべてを加算式にすると初動が遅れる。
そこで「確定シグナルだけは即遮断・それ以外は段階的に評価」の構成にした。
-
第 1 層(即遮断): 単発 API 呼び出しで悪意 100% 確定するものだけ。
「直前数秒でユーザーが何も操作していない」を必須条件にしている
(正規サイトはボタンを押した時にしか該当機能を呼ばない) -
第 2 層(加算): 一つでは黒と言えない兆候を合算。閾値到達で発火。
例:キーボードハック・マウス操作の妨害・全画面表示 など -
第 3 層(言葉狩り): タイトル偽装・恫喝文言・偽 CAPTCHA などの文脈系。
加点材料の上限を敷いていて、文言だけでは警告に届かない設計にしてある(正規のニュース記事や注意喚起ページ対策)
誤検知対策
技術的にはここが一番、詐欺ダメ.com のノウハウが効いた場所だ。
偽通販検出で「誤検知 1 件で信用を失う」を何度も見てきた蓄積が、そのままサポート詐欺対応の設計に流れ込んでいる。
サポート詐欺の検知は「セキュリティ企業のニュース記事」「画像・動画ビューア」で誤爆しやすい。
以下の 3 段構えで潰した。
① ホワイトリスト
国内主要紙・IT メディア・国内外セキュリティ企業を明示的に allow list に載せる。
セキュリティ系ドメインは自社製品や事件をそのまま記事化するため、素直に走らせると「注意喚起ページで自社製品が誤検知する」という笑えない事態が起きる。
「セキュリティ企業だけは記事の中身に関係なく素通し」という割り切りが要る。
② 「これは記事だ」の構造判定
以下のような手がかりを加点し、閾値超えで検知を止める。
- 記事用のタグの存在
- 公開日時タグの有無
- OGP のタイプ指定
- ナビゲーション / フッター構造
- 本文の文字数(一定以上)
- ページ内リンクの本数(一定以上)
ただし、記事の見た目に偽装したサイト対策として、「画面いっぱいを覆う要素があれば逆に無効化」 といった打ち消し条件も入れている。
記事構造スコアだけで判断すると、記事に化けた詐欺サイトを取り逃がす。
③ 単発強すぎるシグナルの重み下げ
正規のフルスクリーンビューアで誤爆した兆候は、単独発火をやめ、他の兆候と合算されて初めて発火する扱いに変更した。
設計順序の話
「単発では強すぎる」シグナルを見つけた時、閾値を下げるより先に、どの誤検知パターンで暴発するかをリストアップしてから調整する。
順序を逆にすると、閾値をいじるだけで検知漏れを増やすことになりやすい。
シグナルを「削る勇気」がないと、単発誤検知が積み重なって使うたび、邪魔になる拡張が出来上がる。
特に第 1 層のような即遮断層では、条件を極限まで狭めておく必要がある。
「加える勇気」と「削る勇気」を、ちゃんと同じ重みで持つ。
公開後に踏んだ大きな失敗:window API を凍結してはいけない
実装で一番痛かった話を書く
第 1 層の hook は window.atob などを拡張版に差し替えて実現している。
差し替える書き方は複数あり、素直に書くと 「二度と上書きできない」形 にできる(writable: false, configurable: false)。
サポート詐欺側が拡張の hook を潰しに来る対策として、当初はこの「凍結」方式を採用した。
が、これが最大の落とし穴だった。
広告計測などの正規ライブラリも、同じ関数を差し替えて動くことがある。
「二度と上書きできない」まで固めると、正規ライブラリが同じ関数を書き換えようとした時に押し合いになり、ページのメインスレッドが応答不能になる。
実際に公開後、広告計測を載せた大手ニュースサイトで再現した。
「拡張機能を入れた瞬間、特定のサイトでタブが固まる」という最悪の障害だ。
解は setter を握り潰す accessor への切り替え。
Object.defineProperty(window, 'atob', {
get() { return wrappedAtob; },
set() { /* 黙って無視 */ },
configurable: true,
});
- サポート詐欺の
window.atob = ...は静かに無視される → 拡張の hook は生き残る - 正規ライブラリの
defineProperty差し替えは失敗するが例外は投げない → ライブラリは壊れない
「敵だけを弾く」より「味方を巻き込まない」を優先する。
ブラウザという共有空間に手を入れる拡張機能の設計原則として、これは強く推したい。
テストと配布
- 検体 13 本を 1 本ずつ per-signal 個別テスト + 組合せテスト で押さえた
- Jest で 140 本超のユニットテスト、判定ロジックの回帰を防ぐ
- 主要ファイルはカバレッジ 90% 以上を維持
- ブランチ運用は
feature/* → develop → mainの 3 段、main直コミットは禁止 - Chrome Web Store 提出時は毎回、追加権限の理由書を添えて審査に出す
まとめ
- ブラックリストでは守れない。Chrome 拡張で ページごと差し替える ことで、握られた仕組みを個別解除する負債を持たなくて済む
- 目的は 致命傷(電話・遠隔・送金・クレカ)の阻止だけに絞る。
一時的な画面奪取まで完全阻止しようとすると副作用と切りがない - MAIN world hook は 「敵だけを弾く」より「味方を巻き込まない」 が最優先。
window API を凍結してはいけない - ここに書いた設計判断・誤検知対策・「削る勇気」といった知見の大半は、詐欺ダメ.com 本体で偽通販を日々解析してきた蓄積からのフィードバックだ。
単発の Chrome 拡張プロジェクトとしてゼロから始めていたら、ここまで副作用に慎重な設計にはならなかった
サービス本体: 詐欺ダメ.com

