• Anonymous
外部連結安全最佳實踐:target='_blank' 與 rel='noopener'
了解為什麼外部連結需要特殊處理,以及如何正確使用 target='_blank'、rel='noopener' 和 rel='noreferrer' 來保護使用者安全
#security
#html
#links
#best-practices
每個網站都會有外部連結,但你知道不正確的外部連結可能帶來安全風險嗎?本文將深入探討如何安全地處理外部連結。
目錄
問題背景
常見的外部連結寫法
<!-- 許多開發者這樣寫外部連結 -->
<a href="https://github.com/your-repo" target="_blank">
View on GitHub
</a>
看起來很正常,對吧?但這裡隱藏著一個安全漏洞。
target=“_blank” 的原始行為
當使用 target="_blank" 時:
- 連結在新分頁開啟
- 新分頁可以透過
window.opener存取原始頁面 - 新分頁可以使用
window.opener.location將原始頁面導向到任何 URL
Tabnabbing 攻擊
攻擊流程
1. 使用者在 yoursite.com 點擊連結
↓
2. 連結開啟 evil-site.com(看起來無害的網站)
↓
3. evil-site.com 執行:
window.opener.location = 'https://phishing-yoursite.com'
↓
4. 使用者的原始分頁被悄悄導向到釣魚網站
↓
5. 使用者切回「原始」分頁,看到登入頁面
↓
6. 使用者以為 session 過期,輸入帳密
↓
7. 帳密被盜 💀
實際案例
這個攻擊被稱為 Reverse Tabnabbing 或 Tab Nabbing。
雖然現代瀏覽器已經開始預設阻擋這個行為(Chrome 88+、Firefox 79+),但:
- 舊版瀏覽器可能仍有風險
- 防禦應該在多層進行
- 明確設定比依賴瀏覽器預設更好
解決方案
加入 rel=“noopener noreferrer”
<!-- ✅ 安全的外部連結 -->
<a
href="https://github.com/your-repo"
target="_blank"
rel="noopener noreferrer"
>
View on GitHub
</a>
每個屬性的作用
| 屬性 | 作用 |
|---|---|
target="_blank" | 在新分頁開啟 |
rel="noopener" | 阻止新窗口存取 window.opener |
rel="noreferrer" | 不發送 Referer header |
rel 屬性詳解
noopener
<a href="..." target="_blank" rel="noopener">
效果:
- 新窗口的
window.opener值為null - 新窗口無法使用
opener.location影響原始頁面 - 這是安全性考量
瀏覽器支援:
- Chrome 49+
- Firefox 52+
- Safari 10.1+
- Edge 79+
noreferrer
<a href="..." target="_blank" rel="noreferrer">
效果:
- 新窗口的
window.opener值為null(包含 noopener 的效果) - 不發送 HTTP Referer header
- 目標網站不知道使用者從哪裡來
何時使用:
- 不想讓目標網站追蹤流量來源
- 隱私考量
- 注意:這會影響目標網站的分析資料
兩者的關係
noreferrer ⊇ noopener
noreferrer 隱含 noopener 的功能,但為了向後相容和明確性,通常兩個都寫:
rel="noopener noreferrer"
nofollow
<a href="..." rel="nofollow">
效果:
- 告訴搜尋引擎不要追蹤這個連結
- 不傳遞 PageRank
何時使用:
- 使用者生成的內容(評論區)
- 贊助連結
- 不信任的連結
這與安全性無關,而是 SEO 考量。
external
<a href="..." rel="external">
效果:
- 純語義化,表示這是外部連結
- 沒有實際功能
- 可用於 CSS 選擇器
a[rel="external"]::after {
content: " ↗";
}
實作範例
基本外部連結
<a
href="https://github.com/username/repo"
target="_blank"
rel="noopener noreferrer"
class="external-link"
>
View Source Code
</a>
Astro 組件
---
interface Props {
href: string;
children: any;
class?: string;
}
const { href, class: className } = Astro.props;
const isExternal = href.startsWith('http');
---
<a
href={href}
class={className}
{...isExternal && {
target: "_blank",
rel: "noopener noreferrer"
}}
>
<slot />
{isExternal && <span aria-hidden="true"> ↗</span>}
</a>
React 組件
interface ExternalLinkProps {
href: string;
children: React.ReactNode;
className?: string;
}
export function ExternalLink({ href, children, className }: ExternalLinkProps) {
const isExternal = href.startsWith('http');
return (
<a
href={href}
className={className}
{...(isExternal && {
target: "_blank",
rel: "noopener noreferrer"
})}
>
{children}
{isExternal && <span aria-hidden="true"> ↗</span>}
</a>
);
}
CSS 視覺提示
/* 外部連結加上箭頭圖示 */
a[target="_blank"]::after {
content: " ↗";
font-size: 0.8em;
}
/* 或使用 SVG 圖示 */
a[target="_blank"]::after {
content: "";
display: inline-block;
width: 1em;
height: 1em;
margin-left: 0.2em;
background-image: url("data:image/svg+xml,...");
background-size: contain;
}
自動化檢查
ESLint 規則
使用 eslint-plugin-jsx-a11y:
// .eslintrc.js
module.exports = {
plugins: ['jsx-a11y'],
rules: {
'jsx-a11y/anchor-is-valid': 'warn',
},
};
HTML Linter
使用 htmlhint:
// .htmlhintrc
{
"attr-unsafe-chars": true
}
自定義腳本檢查
// scripts/check-external-links.js
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const files = glob.sync('src/**/*.{astro,html,tsx}');
files.forEach(file => {
const content = fs.readFileSync(file, 'utf8');
// 找出所有外部連結
const regex = /href=["']https?:\/\/[^"']+["'][^>]*>/g;
const matches = content.match(regex) || [];
matches.forEach(match => {
if (match.includes('target="_blank"') &&
!match.includes('rel="noopener')) {
console.warn(`⚠️ ${file}: Missing rel="noopener" on external link`);
console.warn(` ${match.substring(0, 80)}...`);
}
});
});
Git Hook(pre-commit)
#!/bin/sh
# .husky/pre-commit
# 檢查是否有不安全的外部連結
grep -rn 'target="_blank"' src/ | grep -v 'noopener' && {
echo "❌ Found target=\"_blank\" without rel=\"noopener\""
exit 1
}
exit 0
進階考量
什麼時候不需要這些屬性
內部連結
<!-- 內部連結不需要 noopener -->
<a href="/about" target="_blank">
About Us
</a>
因為 window.opener 指向的是你自己的網站,沒有安全風險。
但是,一般來說內部連結也不應該用 target="_blank"。
同源連結
如果連結是同一個網域(same-origin),瀏覽器的同源策略已經提供保護。
什麼時候應該保留 referrer
<!-- 合作夥伴可能需要追蹤流量來源 -->
<a
href="https://partner-site.com?ref=yoursite"
target="_blank"
rel="noopener" <!-- 只用 noopener,不用 noreferrer -->
>
Partner Site
</a>
效能考量
target="_blank" 還有另一個問題:預設情況下,新分頁與原始分頁共享同一個渲染進程。
這可能導致:
- 新分頁的 JavaScript 影響原始頁面的效能
- 記憶體使用增加
rel="noopener" 會讓新分頁在獨立的進程中運行,這也是效能優化。
總結
最佳實踐清單
- ✅ 所有外部連結加上
target="_blank"(如果需要新分頁) - ✅ 所有
target="_blank"連結加上rel="noopener noreferrer" - ✅ 考慮加上視覺提示(圖示)表明這是外部連結
- ✅ 使用自動化工具檢查遺漏
- ✅ 內部連結避免使用
target="_blank"
快速模板
<!-- 外部連結標準模板 -->
<a
href="https://example.com"
target="_blank"
rel="noopener noreferrer"
>
Link Text
</a>
相關安全標頭
除了 HTML 屬性,也可以使用 HTTP 標頭:
# 強制所有連結使用 noopener
Cross-Origin-Opener-Policy: same-origin
但這是更進階的主題。