クリックジャッキングを根本から防ぐ実践ガイド
クリックジャッキングとは、ユーザーの意図しないクリック/タップを“見えないUI”や重ね合わせで誘導する攻撃である。
1. まずは全体像(なぜ危険か?)
- 攻撃者サイトに透明(または極薄)な であなたのサイトを読み込み、本物のボタンを利用者に押させる。
- 結果として、送金・購入・設定変更・SNSシェアなどが、ユーザーの気づかないうちに実行される。
- 別名:UI Redressing。派生として Likejacking / Cursorjacking / Tapjacking(モバイル) など。
図:攻撃の流れ(概念図)
┌────────────┐ 1. 誘導
│ 攻撃者のページ │ ─────────▶ 「プレゼントを受け取る」等の餌UI
│ (evil.example) │
├────────────┤ 2. 重ね合わせ
│ 透明iframe │ ─────────▶ <iframe src="https://target.example/confirm" style="opacity:0.01;">
│(本物の画面) │
└────────────┘ 3. クリック
└──▶ ユーザーは餌をクリックしたつもりが本物ボタンを押下
2. 対策の基本方針(結論)
サーバ側ヘッダで “フレーム埋め込み禁止/制御” を行うのが第一。具体的には:
-
CSP の
frame-ancestors(推奨、柔軟で将来性あり) - X-Frame-Options (XFO)(古参・広く効くが柔軟性に欠ける)
- (補助)クライアント側の フレーム検知+UI停止(回避困難にする追加ガード)
ベストプラクティス:CSP
frame-ancestorsを主、XFO を併用し、フレーム化検知でUXを停止。
3. 主要対策の比較
| 対策 | 効果範囲 | 長所 | 注意点 | 推奨度 |
|---|---|---|---|---|
CSP: frame-ancestors |
サイトがどこから埋め込めるかを宣言 | ドメイン単位で許可リストを柔軟指定、ポリシー一元管理 | 古いブラウザでは未対応の可能性(現行は概ねOK) | ◎(主役) |
X-Frame-Options (DENY/SAMEORIGIN) |
全体/同一オリジンのみ許可 | 実装が簡単、長年の実績 |
ALLOW-FROMは実質非推奨・互換に難 |
○(併用) |
JSフレームバスティング(if (top!==self) ...) |
埋め込まれた場合にUI停止 | 追加の心理的・操作的バリア | JS無効やCSP・クリック透過など完全対策にはならない | △(補助) |
| SameSite Cookie | CSRFには有効 | クリックジャッキング単体には効かない | 混同注意 | -(別目的) |
4. 実装スニペット(サーバ設定)
4.1 推奨:CSP frame-ancestors
- 全ブロック:
Content-Security-Policy: frame-ancestors 'none';
- 同一オリジンのみ:
Content-Security-Policy: frame-ancestors 'self';
- 特定パートナーのみ許可:
Content-Security-Policy: frame-ancestors 'self' https://partner.example https://*.trusted.com;
重要:HTMLだけでなく、フレーム化され得るレスポンス(エラーページ含む)すべてに付与。
4.2 併用:X-Frame-Options
X-Frame-Options: DENY # どこからもフレーム化不可
# or
X-Frame-Options: SAMEORIGIN # 同一オリジンのみ可
4.3 各種サーバでの書き方
Nginx
add_header Content-Security-Policy "frame-ancestors 'self'" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Apache (httpd.conf / .htaccess)
Header always set Content-Security-Policy "frame-ancestors 'self'"
Header always set X-Frame-Options "SAMEORIGIN"
Node.js (Express)
app.use((_, res, next) => {
res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
next();
});
Spring Security (Java)
http
.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("frame-ancestors 'self'"))
.frameOptions(frame -> frame.sameOrigin())
);
Go (net/http)
func secHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
next.ServeHTTP(w, r)
})
}
CDN/リバースプロキシ使用時は上書きに注意(例:CloudFront/NGINXで必ず
always指定)。
5. 許可付き埋め込みを安全に運用する
-
frame-ancestorsで許可ドメインを最小限に限定。 - 子フレーム側でフレーム検知し、親のオリジン検証+**
postMessageハンドシェイク**を実装。
ハンドシェイク例(簡略)
子(埋め込まれる側)
if (window.top !== window.self) {
window.parent.postMessage({type:'hello', expected:'partner'}, '*');
window.addEventListener('message', (e) => {
if (new URL(e.origin).host.endsWith('partner.example')) {
// OK: 続行
} else {
document.body.innerHTML = '<p>このページは許可されたサイトからのみ表示されます。</p>';
}
});
}
親(埋め込む側)
window.addEventListener('message', (e) => {
if (e.data?.type === 'hello') {
e.source.postMessage({type:'ack', from:location.origin}, e.origin);
}
});
6. フロント側の“補助ガード”(フレーム検知)
意図せずフレーム表示されたら操作不可にする:
<style>
.is-framed * { pointer-events: none !important; }
.is-framed::before { content: '安全のため操作をブロックしました';
position: fixed; inset: 0; display: grid; place-items: center; background: rgba(0,0,0,.6); color: #fff; font: 600 16px/1.4 system-ui; }
</style>
<script>
if (window.top !== window.self) {
document.documentElement.classList.add('is-framed');
}
</script>
これは補助的対策。ヘッダでのブロックが基本。
7. 誤解しやすい関連ヘッダの違い
| ヘッダ/機能 | 目的 | クリックジャッキング防止に直接効く? | 例 |
|---|---|---|---|
CSP: frame-ancestors |
埋め込み元の制御 | はい(主役) |
'none', 'self', 指定ドメイン |
| X-Frame-Options | フレーム化可否 | はい(補完) |
DENY, SAMEORIGIN
|
| COOP/COEP/CORP | 文書・リソースの分離/共有制御 | いいえ(目的が違う) | Spectre対策/共有制御 |
| SameSite Cookie | CSRF軽減 | いいえ(別脅威) | Lax/Strict |
8. 自動テスト/監視に組み込む
- ヘッダ検査:
curl -sI https://your.example | sed -n 's/^Content-Security-Policy: .*$/&/p; s/^X-Frame-Options: .*$/&/p'
-
CIでの失敗条件:
-
frame-ancestorsが欠落 or 意図しないドメインが含まれる - 4xx/5xxエラーページにも付与されていない
-
-
ブラウザ検証:DevTools → Security / Network ヘッダ確認
9. 最小PoC(ローカルで挙動確認)
attacker.html(学習/検証用)
<!doctype html>
<body>
<h1>無料ギフトを受け取る</h1>
<button style="position:relative; z-index:1;">受け取る</button>
<iframe src="https://target.example/confirm"
style="position:absolute; top:0; left:0; width:100%; height:100%; opacity:0.01;">
</iframe>
</body>
→ これが成立しないこと(=フレーム拒否/操作不可)を確認。
10. ありがちな落とし穴
- 200だけに付けて404/500に未設定(エラーページが狙われる)。
- CDN/ロードバランサでヘッダが消える/上書き。
-
ALLOW-FROMを期待する(実質使えない)。 - 一部機能だけ
XFO: ALLOWを試みる(粒度が荒く危険)。 - SPAで静的配信元とAPI配信元で設定が分散し漏れる。
11. モバイルの補足(Tapjacking)
- Androidではオーバーレイによるタップ乗っ取りが近縁の脅威。アプリ側で
filterTouchesWhenObscured等の対策が存在(Webとは別領域)。
12. 導入チェックリスト(30分でやる版)
- 方針決定:基本は
frame-ancestors 'self'。 - サーバ全経路に適用:200/301/302/404/500 を網羅。
- XFO SAMEORIGIN を併用(古いUA向け保険)。
- フロントのフレーム検知で操作停止(補助)。
- CIにヘッダ検査を追加、CDN/反映確認。
13. まとめ
- クリックジャッキングはUI重ね合わせによる意図しない操作の強制。
-
CSP
frame-ancestorsを主軸に、XFO併用 + フレーム検知で多層防御。 - 許可が必要な埋め込みは許可ドメイン最小化と**
postMessageハンドシェイク**で安全に。
今日から実装するなら:
frame-ancestors 'self'+X-Frame-Options: SAMEORIGINを “常に” 返す。これが最小で強力。