target="_blank"未配rel="noopener noreferrer"是明确漏洞,会致原页面被篡改、清空或卡顿;noopener切断window.opener控制链,noreferrer防referer泄露,二者缺一不可且须小写无空格。

target="_blank"不加rel="noopener noreferrer"就是漏洞
不是“可能有风险”,而是浏览器明确允许新页面篡改原页面的地址栏、DOM,甚至触发卡顿。只要写了target="_blank"却没配rel="noopener noreferrer",就等于把window.opener这把钥匙直接交给对方。
典型攻击链:window.opener.location.replace("https://fake-login.example")——你点个外链,用户一返回发现已在钓鱼页;window.opener.document.write("")——原页面被清空;两个标签页共享渲染进程,对方跑死循环,你这页就卡住。TikTok 2023 年真实被利用过。
-
noopener是核心:强制新页面的window.opener为null,从源头切断控制链 -
noreferrer是补充:不发Referer头,防来源泄露;它在部分旧版 Safari 中不保证切断window.opener,不能替代noopener - 二者必须同时出现,且只在
target="_blank"(或其它非_self值)时生效;单独写rel="noopener"会被浏览器忽略 -
rel必须小写、无连字符,rel="NOOPENER"或rel="noopener noreferrer "(末尾空格)都无效
哪些地方最容易漏掉rel属性
手动写 HTML 很容易补上,但动态生成环节几乎全是盲区:
- 富文本编辑器输出(如 CKEditor、TinyMCE)默认不加
rel,导出的 HTML 直接带target="_blank"裸奔 - CMS 或博客系统自动生成的外链(评论区、文章内嵌链接),往往只过滤
href,不校验rel - Vue / React 模板中
v-html或dangerouslySetInnerHTML渲染的用户输入内容,target="_blank"可能被注入,但rel不会自动补 - 第三方组件库(如旧版 Ant Design 的
Link)默认不加rel,升级前需手动 patch -
window.open(url, "_blank")调用时没传'noopener'特性字符串,比如window.open(url, "_blank", "noopener")才有效
target的合法值只有五个,乱写等于失效
target不是随便填的字符串,W3C 明确只认以下五类:
-
_self:当前标签页(可省略) -
_blank:唯一推荐用于“新开页”的值,注意必须小写、无空格 -
_parent:仅在嵌套<iframe></iframe>场景中有效 -
_top:跳出所有 iframe 层级,在顶层窗口加载 - 自定义框架名(如
myFrame):需页面中真实存在对应<iframe name="myFrame"></iframe>
常见错误写法:target="blank"(漏下划线)、target="_new"、target="_BLANK"(大小写错误)、target="myFrame "(末尾空格)——这些都会 fallback 到_self或行为不可预测,且完全不触发rel="noopener"保护。
怎么验证是否真的安全了
肉眼检查几乎无效,尤其面对 CMS 输出或 SSR 渲染结果:
- 用 Lighthouse 扫描,勾选“Best Practices”,它会报
Links with target="_blank" must have rel="noopener"警告 - 正则匹配:
/<a>]*target=["']_blank["'][^>]*>/gi</a>,再逐条确认是否含rel=["'][^"']*noopener[^"']*["'] - 打开 DevTools → Console,执行
document.querySelectorAll('a[target="_blank"]'),遍历检查每个元素的rel属性是否包含noopener - 不要依赖 Chrome 88+ 的隐式
noopener:Safari 和 Firefox 仍需显式声明,生产环境必须写死
真正难的不是写对这一行,而是让所有动态入口——从 Markdown 渲染器到 CMS 插件,再到前端组件封装——统一守住这条底线。一旦漏掉一个,整站外链就等于裸奔。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











