用 .htaccess 加 X-Frame-Options 防点击劫持
点击劫持攻击会让用户在不知情的情况下点击网银转账或修改密码确认按钮。本文从 2008 Twitter Don't Click 蠕虫和 2018 Facebook Likejacking 案例切入,把 X-Frame-Options 三种取值、Apache 与 Nginx 与 PHP 三种添加方式对照、CSP frame-ancestors 现代化替代、CDN 反向代理覆盖响应头排查、6 个安全头同步加固完整组合都讲清楚。
360 网站安全检测、Tencent Habo、Mozilla Observatory 这类工具扫描中国大陆站点时,几乎人人都见过那条"X-Frame-Options 头未设置"的提示。多数站长会糊里糊涂地把它列为"轻微级别"忽略掉,但这个看似不起眼的响应头是防御点击劫持(Clickjacking)攻击最便宜的一道防线。本文从点击劫持攻击的原理切入,把 X-Frame-Options 的三个取值(DENY / SAMEORIGIN / ALLOW-FROM)、Apache .htaccess 与 Nginx 与 PHP 三种添加方式对照、CSP frame-ancestors 现代化替代方案、CDN 反向代理覆盖响应头的常见踩坑、双写兼容老浏览器的策略、Cloudflare Transform Rules 配置、HSTS 与 X-Content-Type-Options 与 Referrer-Policy 等同步加固的安全头组合都讲清楚。
点击劫持到底是什么攻击
攻击原理:透明 iframe 覆盖陷阱
点击劫持(Clickjacking,又叫 UI redressing)的核心是:攻击者把目标网站的页面用 <iframe> 嵌入自己控制的页面,再用 CSS 把 iframe 设成完全透明(opacity: 0)覆盖在伪装的诱饵按钮上方。用户在攻击者站点上看到"点击领取奖品"或"点击播放视频"按钮,实际上点的是被透明 iframe 加载的目标站点的"确认转账"或"修改密码"按钮。
2008 Twitter Don't Click 蠕虫
最经典的案例是 2008 年 Twitter Don't Click 蠕虫。攻击者在第三方页面嵌入透明 iframe 加载 Twitter 的发推界面,诱导用户点击"Don't Click"按钮——实际上点的是 Twitter 上预填好"Don't Click http://malicious.com/"内容的发送按钮。几小时内整个 Twitter 被这条推文刷屏。这次事件直接催生了 X-Frame-Options 标准(IETF RFC 7034)。
2018 Facebook Like 劫持
2018 年 Facebook 修复一组 Likejacking 漏洞——攻击者在第三方页面嵌入 Facebook Like 按钮的透明 iframe,让访问该页面的用户在不知情的情况下"点赞"了攻击者指定的内容。修复方案就是 Facebook 的所有交互页面都加了 X-Frame-Options: DENY。
受害者最多的场景
| 场景 | 攻击效果 |
|---|---|
| 网银转账确认页 | 用户被骗点确认按钮转账给攻击者 |
| 修改密码页 | 用户被骗设置攻击者预填的弱密码 |
| OAuth 授权确认 | 第三方应用被授予不该有的权限 |
| 电商一键购买 | 用户莫名其妙买到指定商品 |
| 社交关注/取关 | 账号关注关系被批量篡改 |
| 评论/点赞 | 用户被代为表态产生品牌损害 |
所有这些后果只要在响应头加一行 X-Frame-Options: SAMEORIGIN 就能根除。配置成本几乎为零,效果立竿见影——这是为什么所有安全合规检测都把它列为基本要求。
X-Frame-Options 的三种取值
DENY:完全拒绝任何 iframe 嵌入
最严格的设置。任何页面用 <iframe> 加载本站点的页面,浏览器都拒绝渲染——不论是同源还是跨源。哪怕本站自己的页面想嵌入自己的预览,也会被拒。
适用场景:纯后台管理系统、网银 / 支付确认页等不存在自家 iframe 用例的页面。
SAMEORIGIN:只允许同源 iframe
仅允许"协议 + 域名 + 端口"完全相同的页面嵌入。https://example.com/foo 可以嵌 https://example.com/bar,但 https://other.com 嵌 https://example.com 会被拒。
适用场景:90% 的常规站点。后台预览、富文本插件、内嵌帮助文档等同源 iframe 用例不会被打断,跨站嵌入完全防护。
ALLOW-FROM uri:白名单单个域名
允许指定单个域名嵌入。但这个值在 2018 年 Chrome 76 与 Edge 后被废弃,Firefox 70 后也废弃。现代浏览器看到 ALLOW-FROM 会回退到 DENY。
所以 ALLOW-FROM 现在等于"对现代浏览器是 DENY"。要做白名单建议用 CSP 的 frame-ancestors 替代——后者支持多个域名、通配符、scheme 匹配,功能强很多。
三种取值对比
| 取值 | 同源嵌入 | 跨源嵌入 | 白名单 | 现代浏览器支持 |
|---|---|---|---|---|
| DENY | 拒绝 | 拒绝 | 否 | 全部支持 |
| SAMEORIGIN | 允许 | 拒绝 | 否 | 全部支持 |
| ALLOW-FROM uri | 视情况 | 仅指定域名 | 是 | 已废弃,等同 DENY |
九成项目用 SAMEORIGIN。极少数纯后台用 DENY。需要白名单跨站嵌入直接跳到 CSP frame-ancestors。
三种添加方式的取舍
方式 1:Web 服务器配置(最干净)
Nginx:
add_header X-Frame-Options SAMEORIGIN always;放在 server 块或 location 块里。always 关键字确保 4xx / 5xx 错误页也带这个头(不带 always 时只有 200 响应才发)。
Apache:
Header always set X-Frame-Options SAMEORIGIN放在 httpd.conf 或 vhost 配置里。需要 mod_headers 模块启用:a2enmod headers。
这是最干净的方式——一处配置全站生效,性能开销几乎为零(响应头由 Web Server 直接附加)。但需要 root 权限或主机面板权限。
方式 2:PHP 代码层 header()
<?php
header('X-Frame-Options: SAMEORIGIN');
?>这行必须在任何 HTML 输出之前执行,不然 PHP 报 headers already sent 警告。Typecho、WordPress 这种 CMS 可以放在主题的 header.php 顶部,或者更稳妥的写一个插件挂在 init 钩子上。
优点:无需服务器权限,主题 / 插件层面就能配。缺点:每次请求都要走 PHP,少 1-2ms 性能。
方式 3:.htaccess(FTP-only 用户的首选)
这是给只有 FTP 权限、没有主机面板的同学准备的方案。.htaccess 是 Apache 的目录级配置文件,扔到网站根目录立刻生效:
<IfModule mod_headers.c>
Header always set X-Frame-Options SAMEORIGIN
</IfModule>外面包一层 <IfModule mod_headers.c> 是好习惯——万一服务器没启用 mod_headers,整个 .htaccess 不会因为这一行报 500。
三种方式对比
| 对比项 | Nginx/Apache 配置 | PHP header() | .htaccess |
|---|---|---|---|
| 需要权限 | root / 主机面板 | FTP | FTP |
| 生效范围 | 全站 | 有 header() 的 PHP | 所在目录 |
| 性能开销 | 几乎为零 | 每请求 1-2ms | 每请求 1-3ms |
| 错误页是否带头 | 带(加 always) | 不带 | 带(加 always) |
| 静态资源是否带头 | 带 | 不带 | 带 |
| 修改是否要重启 | nginx -s reload | 无 | 无 |
选优先级:服务器配置 > .htaccess > PHP header()。能用前者就别用后者。
.htaccess 完整加固方案
同时加多个安全头
X-Frame-Options 单独配可惜了,建议把常用的安全头一起加上:
<IfModule mod_headers.c>
Header always set X-Frame-Options SAMEORIGIN
Header always set X-Content-Type-Options nosniff
Header always set Referrer-Policy strict-origin-when-cross-origin
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Content-Security-Policy "frame-ancestors 'self'; default-src 'self' 'unsafe-inline'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"
</IfModule>各个头的作用对照
| 响应头 | 作用 | 风险 |
|---|---|---|
| X-Frame-Options | 防点击劫持 | iframe 嵌套被禁 |
| X-Content-Type-Options: nosniff | 防 MIME 类型嗅探攻击 | 无 |
| Referrer-Policy | 控制 Referer 头泄露 | 无 |
| Permissions-Policy | 禁用敏感 Feature | 正常用例需调整 |
| Strict-Transport-Security | 强制 HTTPS | HTTP 站点不能加 |
| Content-Security-Policy | 综合防 XSS / 资源加载 | 影响内联脚本 |
HSTS 的风险提示
HSTS(Strict-Transport-Security)一旦设置浏览器会强制 HTTPS 访问指定时间内的所有请求。如果你站点 HTTPS 配置不完善(证书过期、子域名没装证书等)会让用户完全无法访问。建议:
- 初次部署 HSTS 时把
max-age设短(比如 300 秒 = 5 分钟)做测试。 - 确认全站 HTTPS 稳定 1-2 周后再调到 31536000(1 年)。
includeSubDomains慎用——所有子域名必须都支持 HTTPS 才能启用。- 不要轻易加
preload——加了之后从浏览器内置 HSTS 列表里移除流程很麻烦。
验证响应头是否生效
浏览器 DevTools
F12 → Network → 刷新页面 → 点第一个 Document 请求 → Response Headers。找 X-Frame-Options 那一行,确认 value 是 SAMEORIGIN。
命令行 curl
curl -I https://your-domain.com输出包含所有响应头,找 X-Frame-Options 行。如果没有,先检查 mod_headers 是否启用:
apachectl -M | grep headers
# 应输出 headers_module (shared)
# 没输出说明模块没加载,需要 a2enmod headers 然后 service apache2 restart在线检测工具
- securityheaders.com:综合评级 A+ 到 F,列出哪些头缺失。
- Mozilla Observatory:更严格的评分,给出具体改进建议。
- 360 网站安全检测:国内常用,适合给客户出报告。
iframe 嵌入测试
本地写一个 HTML 文件:
<!DOCTYPE html>
<html><body>
<h1>测试嵌入</h1>
<iframe src="https://your-domain.com" width="800" height="600"></iframe>
</body></html>在浏览器打开。如果设置生效,iframe 区域应该是空白或显示"example.com refused to connect"。Console 也会有 X-Frame-Options 拒绝错误。
CSP frame-ancestors 是未来方向
为什么 CSP 替代 X-Frame-Options
X-Frame-Options 在 W3C 标准里已被标记为 Deprecated,新标准是 CSP(Content Security Policy)的 frame-ancestors 指令。功能比 X-Frame-Options 强:
- 支持多个白名单域名(
frame-ancestors 'self' https://trusted-partner.com https://another.com)。 - 支持通配符(
frame-ancestors *.example.com)。 - 支持 scheme 匹配(
frame-ancestors 'self' https:表示同源 + 任意 HTTPS)。 - 没有 ALLOW-FROM 那种废弃问题。
双写兼容老浏览器
建议双写——X-Frame-Options 给老浏览器(IE 11、老 Android)兜底,CSP frame-ancestors 给现代浏览器:
<IfModule mod_headers.c>
Header always set X-Frame-Options SAMEORIGIN
Header always set Content-Security-Policy "frame-ancestors 'self';"
</IfModule>如果两者都设置且策略不同,现代浏览器以 CSP 为准。配置不一致没关系——CSP 会覆盖 X-Frame-Options。
白名单单个或多个域名
Header always set Content-Security-Policy "frame-ancestors 'self' https://partner1.com https://partner2.com;"这就允许同源页面 + 两个合作伙伴域名嵌入。如果完全不允许嵌入:
Header always set Content-Security-Policy "frame-ancestors 'none';"常见踩坑与排查
.htaccess 已有内容覆盖了原有规则
很多人直接整段覆盖 .htaccess 结果把人家的伪静态规则、防盗链规则、错误页规则都覆盖了,站点 404。每次都先下载原文件备份再追加,不要覆盖。如果不知道现有内容是什么,先备份再说。
CDN 或反向代理覆盖响应头
站点前面挂了 Cloudflare、阿里云 CDN、腾讯云 CDN 时,原站设置的头可能被 CDN 替换或剥离。需要在 CDN 控制面板里同步配置:
- Cloudflare:Rules → Transform Rules → Modify Response Header → Add Static。
- 阿里云 CDN:缓存配置 → HTTP 头部设置 → 添加。
- 腾讯云 CDN:高级配置 → HTTP Header 配置。
验证方式:直接 curl 站点域名(走 CDN)的响应头,对比 curl 源站 IP 的响应头。两者应该一致。
开发本地 iframe 嵌入预览失效
同源策略里端口也算源的一部分,http://localhost:3000 嵌 http://localhost:8000 是跨源。开发环境下要预览:
- 临时改成
X-Frame-Options: DENY(开发完恢复 SAMEORIGIN)。 - 或者前端开发服务器配代理把后端请求代理到同端口。
- 或者用 CSP frame-ancestors 加白名单 localhost。
WordPress / Typecho 等 CMS 后台预览失效
设置 SAMEORIGIN 后如果同源仍被拦,检查后台和前台是否真同源——端口、协议、子域、路径前缀都不能差。常见情况是后台用 https://example.com/wp-admin/ 但前台用 http://example.com/(HTTP),协议不一致就被认为跨源。修复 HTTPS 一致性即可。
360 检测显示已修复但 Chrome 还是报旧值
清浏览器缓存或用无痕模式打开。CDN 缓存也要刷——Cloudflare 在 Caching 面板手动 Purge。验证时优先用 curl 拿真实响应头,浏览器有时候缓存得太狠。
常见问题解答
设置后我自己后台预览功能也不能用了怎么办
大概率你用的是 DENY 改成 SAMEORIGIN 即可。如果同源仍然被拦,检查后台和前台是不是真同源——端口、协议、子域都不能差。常见情况是后台用 admin.example.com 但前台用 example.com,子域不同算跨源。要么改成同子域,要么用 CSP frame-ancestors 加白名单。
Nginx 的话怎么配
直接在 server 块里加 add_header X-Frame-Options SAMEORIGIN always 那一行。Nginx 没有 .htaccess,所有配置都在 nginx.conf 或 conf.d 下。改完 nginx -t 验证语法再 systemctl reload nginx。注意 add_header 在 location 块里会覆盖外层 server 块——如果某个 location 已经有 add_header 又想加新头,必须在那个 location 里把所有头都重新写一遍。
360 检测显示已修复但 Chrome 还是报旧值怎么处理
清浏览器缓存或者用无痕模式打开。CDN 缓存也要刷——Cloudflare 在 Caching 面板手动 Purge。验证时优先用 curl 拿到真实响应头,浏览器有时候缓存得太狠。如果 curl 显示新值但浏览器还是旧值就是浏览器缓存问题,反之就是 CDN 或反向代理问题。
能不能只对部分页面设置 SAMEORIGIN 其他页面 DENY
可以。.htaccess 里用 FilesMatch 或 If 条件块针对特定 URL 设不同值。但这种做法维护成本高建议要么全站统一 SAMEORIGIN,要么细粒度场景改用 CSP 的多个策略文件。CSP frame-ancestors 支持更灵活的白名单,更适合这种场景。
HTTP 站点能加 HSTS 头吗
不能。HSTS 是强制 HTTPS 的指令,只在 HTTPS 响应里有意义。HTTP 站点的浏览器会忽略这个头。如果你的站点是 HTTP,先把 HTTPS 部署好(Let's Encrypt 免费证书)再加 HSTS。HTTP 直接加 HSTS 既无效又可能在切换协议时出 bug。
X-Frame-Options 和 CSP frame-ancestors 冲突时谁优先
现代浏览器以 CSP 为准。两者都设置且策略不一致时,浏览器优先看 CSP frame-ancestors。所以双写时不用担心策略冲突——CSP 自动接管。但如果你只想看 X-Frame-Options 生效(比如调试老浏览器),可以临时去掉 CSP 那行。
设置后第三方支付页面的 iframe 跳转失败
支付宝、微信支付的部分支付方式确实用 iframe 嵌入支付页。如果你的网站需要内嵌支付 SDK 的 iframe,并且支付方域名固定,建议用 CSP frame-ancestors 加白名单——而不是 X-Frame-Options(因为 X-Frame-Options 是单值不支持白名单)。或者把支付环节改成完整跳转到支付方页面(顶级跳转),支付完成后再回跳。
Permissions-Policy 设置后某个 Feature 不能用怎么办
Permissions-Policy 的语法是允许列表写法 feature=(allowlist)。空括号意味着完全禁用。要允许同源用某个 Feature 写 feature=(self),允许特定域名写 feature=("https://trusted.com")。常见错误是直接 geolocation=() 后忘了把同源加回来,导致自家网站调用地理位置失败。修复就是改成 geolocation=(self)。
共享主机 mod_headers 被禁用怎么办
少数廉价共享主机为了限制资源会禁用 mod_headers。这种情况只能:用 PHP header() 在主题层面设置(生效范围仅限通过 PHP 渲染的页面,静态资源不带头);或者换主机。云主机(阿里云、腾讯云、Vultr)99% 都支持 mod_headers,没必要为这点限制忍受。
总结
X-Frame-Options 是 Web 安全里"投入产出比最高"的配置之一——加一行 .htaccess 或一行 Nginx 配置,就能根除点击劫持类攻击。配上 X-Content-Type-Options、Referrer-Policy、Permissions-Policy、HSTS、CSP 这一组现代安全头,你的站点在 securityheaders.com 上的评级可以从 F 直接拉到 A+。
如果只是接 360 安全检测的那一条提示做应急修复,本文方案 A(Nginx / Apache 配置或 .htaccess)就够用。如果是要做认真的安全加固,建议直接上 CSP + 完整安全头组合,并把 securityheaders.com 评级 A+ 作为长期持有的指标。安全这件事是"持有"不是"达成"——配置只是起点,每隔 3-6 个月扫一次确保没回退。
本文标题:《用 .htaccess 加 X-Frame-Options 防点击劫持》
本文链接:https://zhangwenbao.com/htaccess-x-frame-options.html
版权声明:本文原创,转载请注明出处和链接。许可协议: CC BY-NC-SA 4.0