仅设 x-frame-options 不够防点击劫持,因现代浏览器已忽略该头而优先遵循 csp 的 frame-ancestors;必须同时设置两者且语义一致,引号不可省略,中间件需覆盖所有响应路径并验证实际生效。

为什么只设 X-Frame-Options 不够防点击劫持
现代浏览器(Chrome 90+、Firefox 92+、Safari 15.4+)已逐步忽略 X-Frame-Options,转而优先遵循 Content-Security-Policy 中的 frame-ancestors 指令。如果你只写 c.Header("X-Frame-Options", "DENY"),在新版本浏览器里可能完全不生效——攻击者仍能用 iframe 套住你的登录页或支付确认页。
必须同时设置两个头,且语义一致:一个保兼容性(老浏览器),一个保现代性(新浏览器)。漏掉任意一个,就等于对某类用户敞开了门。
frame-ancestors 'none' 和 'self' 的引号不能省
Content-Security-Policy 的 frame-ancestors 值是 CSP 字符串字面量,单引号必须存在。写成 frame-ancestors none 或 frame-ancestors self 是无效语法,浏览器直接忽略整条 CSP 头。
-
frame-ancestors 'none'→ 完全禁止嵌入(推荐用于后台、登录、账户页) -
frame-ancestors 'self'→ 只允许同源页面嵌入(如app.example.com嵌入dashboard.example.com,需确保二者同域或子域可控) - 若需白名单多个源,写成
frame-ancestors 'self' https://embed.example.com,注意每个源都带引号、空格分隔
中间件里设头的顺序和覆盖风险
Gin 的 c.HTML() 内部会重写 Content-Type 和部分安全头,如果你在中间件里设了 Content-Security-Policy,又在 handler 里调用 c.HTML(),那个 CSP 就会被清掉——这是最常被踩的坑。
- 所有安全头必须在
c.Next()之前设置,且放在中间件最外层 - 不要在模板渲染逻辑里补头;也不要在
c.Redirect()或c.JSON()后再试图改头——它们已触发写响应 - 静态文件(
r.Static())默认不走中间件,需单独为/static/路径注册安全头中间件,或改用r.StaticFS()配合自定义http.FileSystem
例外路径(如 /embed/)怎么安全放开
有些业务确实需要被嵌入,比如数据看板嵌入客户官网。这时不能全局 DENY,但也不能无条件放行——必须精确控制范围。
- 用
strings.HasPrefix(c.Request.URL.Path, "/embed/")判断路径前缀,避免误匹配/embeddings这类干扰路径 - 对例外路径,
X-Frame-Options设为SAMEORIGIN(注意大小写,不能小写sameorigin) - 对应 CSP 改为
frame-ancestors 'self',而非'none';若需第三方嵌入,只加可信域名,如frame-ancestors 'self' https://customer.com - 第三方统计脚本(如百度热点追踪)若依赖 iframe 加载,也得在 CSP 里显式加入,例如:
frame-ancestors 'self' https://tongji.baidu.com
curl -I 结果里,配置就只是纸面功夫。











