跳到主要内容

点击劫持与安全

点击劫持如何"代替用户点击"?​

  • 本质: 恶意页面把受害站点的透明 <iframe> 盖在自己的诱饵按钮上, 用户以为点了诱饵, 实际点到 iframe 里的按钮;
  • 前提: 用户已在受害站点登录 (通常靠"记住我"的 cookie), 一次点击即生效;
  • 先例: Twitter 的 "Follow"、Facebook 的 "Like"、PayPal 都中过招, 现已修复;
  • 步骤:
    • 用任何方式把用户骗到恶意页面;
    • 页面放一个看似无害的链接或按钮 ("click here, very funny");
    • 用绝对定位加 z-index 把受害站点的 <iframe> 叠在按钮正上方, 让目标按钮恰好对准诱饵;
    • 用户点击诱饵 = 点击受害站点按钮;
  • 只骗鼠标/触屏: 键盘输入很难被重定向; 就算用重叠输入框骗焦点, 用户看不到自己敲的内容也会停手;
<style>
iframe {
width: 400px;
height: 100px;
position: absolute;
top: 0;
left: -20px;
opacity: 0; /* 真实攻击全透明, 演示才用 0.5 */
z-index: 1;
}
</style>
<div>Click to get rich now:</div>
<iframe src="https://victim.example/like.html"></iframe>
<button>Click here!</button>
  • 危险程度: 只需截获一次点击, 看似"浅"; 但若攻击者预知点击后会出现的下一个控件, 就能连环诱导, 所以在设计 UI 时很难预判, 漏洞常出现在意想不到的地方;

为什么 top != window 检测不可靠?​

  • 老做法叫 framebusting: 发现自己在 frame 里就自动升到顶层;
if (top != window) {
top.location = window.location;
}
  • 绕过一: 恶意顶层页设置 window.onbeforeunload = () => false;, 用户被问"是否离开此页"时通常选否 (他只看得到顶层页面, 没有理由离开), 于是 top.location 改不动;
  • 绕过二: 恶意页用 <iframe sandbox="allow-scripts allow-forms" src="victim.html"> 嵌入受害页, 因为没给 allow-top-navigation, 改 top.location 被禁止;
  • 结论: 这类脚本检测只是弱防护, 还有更多绕过方式, 不能作为主要防线;

X-Frame-Options 如何阻止页面被嵌套?​

  • 形式: 必须作为 HTTP 响应头发送; 写在 HTML 的 <meta http-equiv="X-Frame-Options" ...> 里会被浏览器忽略;
取值含义
DENY任何情况下都不允许在 frame 中显示;
SAMEORIGIN父文档与自身同源时才允许;
ALLOW-FROM domain父文档来自指定 domain 时才允许;
  • 实例: Twitter 使用 X-Frame-Options: SAMEORIGIN;
  • 副作用: 别人即使有正当理由也不能把你的页面嵌进 frame, 于是需要下面的替代方案;

想让页面能被 iframe 展示时怎么做?​

  • 遮罩法: 用一个铺满页面的 <div> 拦截所有点击, 确认自己在顶层 (或确认不需要保护) 后移除它;
<style>
#protector {
height: 100%;
width: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 99999999;
}
</style>
<div id="protector"><a href="/" target="_blank">Go to the site</a></div>
<script>
// 异源父窗口访问 top.document 会报错, 这里不需要处理
if (top.document.domain == document.domain) protector.remove();
</script>
  • SameSite cookie: 给认证 cookie 加 samesite, 则站点被别的页面用 iframe 打开时不会带上该 cookie, 攻击因未登录而失败, 见 cookie;
  • 局限: 不用 cookie 的场景仍可能被点击劫持, 例如靠 IP 防重复投票的匿名投票站 (无需认证, 无 cookie 可保护);

CSP frame-ancestors 与防御清单是什么?​

  • 新方案: Content-Security-Policy: frame-ancestors ... 直接声明谁可以嵌本页, 粒度比 X-Frame-Options 细 (可列多个来源), 同样必须走 HTTP 响应头;
CSP 取值等价 X-Frame-Options含义
frame-ancestors 'none'DENY谁都不许嵌;
frame-ancestors 'self'SAMEORIGIN只允许同源嵌;
frame-ancestors https://a.testALLOW-FROM只允许列出的来源嵌;
  • 优先级: 两个头同时下发时以 CSP 为准; frame-ancestors 不回落 default-src; 老式的 ALLOW-FROM 多数浏览器已忽略, 应当改用 CSP;
  • 清单:
    • 不该被嵌的页面: 下发 X-Frame-Options: SAMEORIGIN 或 frame-ancestors 'self';
    • 完全不需要被嵌: 用 DENY 或 frame-ancestors 'none';
    • 要嵌第三方的不可信内容: 用 <iframe sandbox>, 见 iframe 内容访问、窗口层次与隔离;
    • 认证 cookie 加 SameSite;
    • 既想允许被嵌又要安全: 用遮罩 <div> 方案;
    • 不要依赖 top != window 脚本检测当主防线;