必须同时使用 rel="noopener noreferrer" 且写法严格,否则 target="_blank" 会因 window.opener 被劫持导致原页面跳转、DOM 清空或卡死;仅 rel="noopener" 才能切断控制链,noreferrer 仅补充隐私防护。

只写 target="_blank" 就上线?等于主动交出原页面控制权。 浏览器明确允许新标签页通过 window.opener 修改你的地址栏、清空 DOM,甚至拖垮渲染进程——这不是推测,是已复现的攻击链。
为什么 target="_blank" 会触发 window.opener 劫持
新标签页一打开,它的 window.opener 就指向你当前页面的 window 对象。跨域也不拦截,哪怕目标站和你毫无关系:
-
window.opener.location.replace("https://fake-login.example")—— 原页面瞬间跳转到钓鱼页 -
window.opener.document.write("")—— 原页面 DOM 被清空重写 - 两个标签共享渲染线程,对方跑死循环,你这页直接卡死
这种攻击叫 Tabnabbing,TikTok 2023 年真实被利用过。不是“可能风险”,而是浏览器默认行为。
rel="noopener noreferrer" 必须同时出现且写法严格
noopener 是安全核心,noreferrer 是隐私补充,二者缺一不可:
-
rel="noopener"强制新页面的window.opener为null,从源头切断控制链 -
rel="noreferrer"防 Referer 泄露,但旧版 Safari 中不保证切断window.opener,不能替代noopener - 必须小写、无空格、无连字符:
rel="noopener noreferrer"✅,rel="NOOPENER"❌,rel="noopener noreferrer "(末尾空格)❌ - 仅在
target="_blank"(或其它非_self值)时生效;单独写rel="noopener"会被浏览器忽略
最容易漏掉 rel 的动态场景
手动写 HTML 很容易补上,但以下环节几乎全是盲区:
- 富文本编辑器输出(如 CKEditor、TinyMCE)默认不加
rel,导出的 HTML 直接裸奔 - 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>场景中有效 -
_top:跳出所有<iframe>层级,在顶层窗口加载 - 自定义框架名(如
myFrame):需页面中真实存在对应<iframe name="myFrame">
常见错误写法:target="blank"(漏下划线)、target="_new"、target="_BLANK"(大小写错误)、target="myFrame "(末尾空格)——这些都会 fallback 到 _self 或行为不可预测,且完全不触发 rel="noopener"。
最麻烦的是:DevTools 不再报错,Lighthouse 也只当建议项。等你发现页面被重定向或卡顿,往往已经晚了——尤其在后台系统、支付页、登录态页面里,这类链接一旦裸奔,就是高危入口。

















