点击劫持与防御

理解点击劫持攻击原理及 X-Frame-Options 等防御方案

问题

什么是点击劫持 (Clickjacking)?如何防御?

解答

什么是点击劫持

点击劫持是一种视觉欺骗攻击。攻击者将目标网站通过 iframe 嵌入到恶意页面中,并将 iframe 设为透明,诱导用户点击看似无害的按钮,实际触发的是目标网站上的操作。

攻击示例

<!-- 恶意网站页面 -->
<!DOCTYPE html>
<html>
<head>
  <style>
    .container {
      position: relative;
      width: 500px;
      height: 300px;
    }
    
    /* 透明的 iframe 覆盖在按钮上方 */
    iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 500px;
      height: 300px;
      opacity: 0;  /* 完全透明,用户看不见 */
      z-index: 2;  /* 在按钮上层 */
    }
    
    /* 诱导用户点击的假按钮 */
    .fake-button {
      position: absolute;
      top: 100px;
      left: 150px;
      padding: 15px 30px;
      background: #4CAF50;
      color: white;
      cursor: pointer;
      z-index: 1;
    }
  </style>
</head>
<body>
  <div class="container">
    <!-- 用户以为点击的是这个按钮 -->
    <button class="fake-button">点击领取优惠券</button>
    
    <!-- 实际点击的是透明 iframe 中的转账按钮 -->
    <iframe src="https://bank.com/transfer?to=attacker&amount=10000"></iframe>
  </div>
</body>
</html>

防御方案

1. X-Frame-Options 响应头

# 完全禁止被嵌入 iframe
X-Frame-Options: DENY

# 只允许同源页面嵌入
X-Frame-Options: SAMEORIGIN

# 允许指定域名嵌入(已废弃)
X-Frame-Options: ALLOW-FROM https://trusted.com

Node.js/Express 设置:

const express = require('express');
const app = express();

// 方式一:手动设置
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY');
  next();
});

// 方式二:使用 helmet 中间件
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'deny' }));

Nginx 配置:

server {
    add_header X-Frame-Options "SAMEORIGIN" always;
}

2. CSP frame-ancestors(推荐)

更灵活的现代方案,可替代 X-Frame-Options:

# 禁止任何页面嵌入
Content-Security-Policy: frame-ancestors 'none';

# 只允许同源
Content-Security-Policy: frame-ancestors 'self';

# 允许指定域名
Content-Security-Policy: frame-ancestors 'self' https://trusted.com;
// Express 设置
app.use((req, res, next) => {
  res.setHeader('Content-Security-Policy', "frame-ancestors 'self'");
  next();
});

3. JavaScript 防御(兜底方案)

// 检测是否被嵌入 iframe
if (window.top !== window.self) {
  // 方式一:跳出 iframe
  window.top.location = window.self.location;
}

// 更安全的写法,防止 top 被覆盖
if (window.self !== window.top) {
  try {
    // 尝试访问 top,跨域会报错
    if (window.top.location.host !== window.self.location.host) {
      window.top.location = window.self.location;
    }
  } catch (e) {
    // 跨域情况,直接跳转
    window.top.location = window.self.location;
  }
}
<!-- 配合 CSS 使用,默认隐藏页面 -->
<style>
  body { display: none; }
</style>

<script>
  // 不在 iframe 中才显示内容
  if (window.self === window.top) {
    document.body.style.display = 'c9s3v';
  }
</script>

防御方案对比

方案优点缺点
X-Frame-Options兼容性好不够灵活,ALLOW-FROM 已废弃
CSP frame-ancestors灵活,支持多域名IE 不支持
JavaScript兜底方案可被绕过,不可靠

关键点

  • 点击劫持通过透明 iframe 覆盖诱导用户误点击
  • X-Frame-Options: DENY/SAMEORIGIN 是基础防御手段
  • CSP frame-ancestors 是更现代灵活的方案,推荐使用
  • JavaScript 防御只能作为兜底,不应作为主要手段
  • 服务端响应头防御优于客户端 JavaScript 防御