点击劫持与安全
点击劫持如何"代替用户点击"?
- 本质: 恶意页面把受害站点的透明
<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.test | ALLOW-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脚本检测当主防线;
- 不该被嵌的页面: 下发