前端安全
前言
安全防护不是安全工程师一个人的事情,而是每一位前端开发者都应当具备的基本素养。本文从前端开发的编码实践出发,聚焦日常开发中最常遇到的安全场景,深入剖析漏洞原理并给出可直接落地的防御代码。与 /security/ 目录下的理论向文档不同,本文更关注"怎么写出安全的代码"——从一行 innerHTML 到一段 CSP 配置,从一次 API 调用到一条 npm install 命令。
XSS 跨站脚本攻击
反射型 XSS:警惕 URL 参数直接入 DOM
反射型 XSS 是最常见的 XSS 类型。恶意脚本作为 URL 参数传递,服务端未经编码直接将其拼入响应 HTML 中返回。攻击者通常通过社交工程诱导用户点击恶意链接来实施攻击。
漏洞代码示例:
// ❌ 危险:直接将 URL 参数插入页面
const keyword = new URLSearchParams(location.search).get('q');
document.getElementById('result').innerHTML = `您搜索的是:${keyword}`;当用户访问 https://example.com/search?q=<img src=x onerror=alert('XSS')> 时,恶意脚本就会执行。
防御编码实践:
// ✅ 安全:使用 textContent 替代 innerHTML
const keyword = new URLSearchParams(location.search).get('q') || '';
document.getElementById('result').textContent = `您搜索的是:${keyword}`;
// ✅ 安全:必须渲染 HTML 时使用 DOMPurify
import DOMPurify from 'dompurify';
document.getElementById('result').innerHTML = DOMPurify.sanitize(
`您搜索的是:${keyword}`
);服务端输出编码同样不可忽视。Go、Java、Node.js 等后端语言在处理模板渲染时,务必对动态内容进行 HTML 实体编码:
// Go 标准库 html/template 自动编码
import "html/template"
tmpl := template.Must(template.New("page").Parse(`<p>关键词:{{.Keyword}}</p>`))// Java 使用 HtmlUtils 进行 HTML 编码
import org.springframework.web.util.HtmlUtils;
String safe = HtmlUtils.htmlEscape(userInput);存储型 XSS:数据库写入不等于安全落地
存储型 XSS 的危害面远大于反射型。恶意脚本被持久化存储在服务端数据库中,每次加载页面都会触发执行,影响所有访问该页面的用户。
典型场景——用户评论/留言板:
// ❌ 服务端直接存储未过滤的用户输入
app.post('/api/comments', (req, res) => {
db.comments.insert({ content: req.body.content }); // 未做任何处理
});
// ❌ 前端直接渲染未编码的评论内容
function renderComments(comments) {
return comments.map(c =>
`<div class="comment">${c.content}</div>` // XSS!
).join('');
}防御编码实践:
// ✅ 前端渲染时使用文本节点
function renderComments(comments) {
const container = document.getElementById('comments');
comments.forEach(c => {
const div = document.createElement('div');
div.className = 'comment';
div.textContent = c.content; // 天然安全
container.appendChild(div);
});
}
// ✅ 服务端存储前做输入净化(深度防御)
import createDOMPurify from 'dompurify';
const DOMPurify = createDOMPurify(window);
const sanitized = DOMPurify.sanitize(req.body.content, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href', 'title']
});DOM 型 XSS:客户端数据源的隐蔽威胁
DOM 型 XSS 完全在浏览器侧发生,不经过服务端。攻击载荷来自 location.hash、document.referrer、window.name、localStorage 等客户端数据源,隐蔽性极高。
// ❌ 危险:从 URL hash 中提取数据直接设置 innerHTML
const hash = location.hash.slice(1);
document.getElementById('content').innerHTML = decodeURIComponent(hash);
// ✅ 安全:使用 textContent 或 createTextNode
document.getElementById('content').textContent = decodeURIComponent(hash);
// ✅ 安全:如果必须渲染 HTML,先净化
import { sanitize } from 'isomorphic-dompurify';
document.getElementById('content').innerHTML = sanitize(decodeURIComponent(hash));DOM XSS 高危 API 清单——在日常编码中格外留意:
| 高危 API | 安全替代方案 |
|---|---|
element.innerHTML = str | element.textContent = str |
element.outerHTML = str | element.insertAdjacentText() |
document.write(str) | document.createTextNode() |
elem.insertAdjacentHTML('beforeend', str) | elem.insertAdjacentText() |
new Function(str) / eval(str) | 避免使用,改用 JSON.parse |
编码与过滤:防御 XSS 的两道利器
第一道:上下文感知编码
XSS 防御的核心是"上下文感知编码"——根据数据将要插入的位置选择正确的编码方式:
// HTML 上下文 → HTML 实体编码
function htmlEncode(str) {
return str.replace(/[&<>"']/g, char => ({
'&': '&', '<': '<', '>': '>',
'"': '"', "'": '''
})[char]);
}
// JavaScript 上下文 → 字符串转义
function jsEncode(str) {
return str.replace(/[\\'"\n\r<\/]/g, char => ({
'\\': '\\\\', "'": "\\'", '"': '\\"',
'\n': '\\n', '\r': '\\r',
'<': '\\x3C', '/': '\\x2F'
})[char]);
}
// URL 上下文 → URL 编码
const safeUrl = `https://api.example.com?redirect=${encodeURIComponent(userInput)}`;第二道:输入过滤
对于富文本输入(如 Markdown 编辑器、富文本编辑器),不能简单编码而是要安全地过滤 HTML:
// 使用 DOMPurify 过滤富文本
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(dirtyHTML, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'img'],
ALLOWED_ATTR: ['href', 'src', 'alt', 'title'],
ALLOW_DATA_ATTR: false,
ADD_ATTR: ['target'] // 额外允许 target 属性
});CSP:防御 XSS 的最后一道防线
内容安全策略(CSP)是浏览器层面的安全护栏。即使攻击者成功注入了恶意脚本,CSP 也能阻止其执行。CSP 应当作为 XSS 防御的兜底措施,而非唯一措施。
最小化 CSP 配置示例:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-随机值';
style-src 'self' 'nonce-随机值';
img-src 'self' data:;
connect-src 'self';
frame-ancestors 'self';
base-uri 'self';
form-action 'self'使用 Nonce 机制配合内联脚本:
<script nonce="EDNnf03nceIOfn39fn3e9h3sdfa">
console.log('只有 nonce 匹配的脚本才会执行');
</script>使用 strict-dynamic 简化 SPA 的 CSP 管理:
Content-Security-Policy: script-src 'nonce-随机值' 'strict-dynamic'strict-dynamic 允许被受信任脚本动态加载的其他脚本也自动获得信任,非常适合单页应用的模块加载场景。
分阶段推行 CSP 的建议:
- Report-Only 模式:先使用
Content-Security-Policy-Report-Only收集违规报告 - 分析调优:根据报告调整策略,消除误报
- 强制执行:确认无误后切换为
Content-Security-Policy
# 先以报告模式运行,确认策略无误
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /api/csp-reportCSRF 跨站请求伪造
攻击原理:信任了不该信任的请求
CSRF 利用的是网站在 Cookie 认证机制上的信任盲区——浏览器会自动携带目标站点的 Cookie 发起请求,而服务端仅靠 Cookie 无法区分请求是用户主动发起还是被攻击者伪造的。
<!-- 攻击者页面中的恶意代码:受害者访问时自动提交 -->
<form id="csrf-form" action="https://bank.example.com/transfer" method="POST" style="display:none">
<input type="hidden" name="toAccount" value="attacker">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf-form').submit();</script>SameSite Cookie:最轻量的 CSRF 防御
SameSite 属性是防御 CSRF 成本最低的方式,无需修改后端业务逻辑,只需调整 Cookie 配置。
// Node.js/Express 配置 SameSite Cookie
res.cookie('sessionId', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax' // 'strict' | 'lax' | 'none'
});// Spring Boot 配置 SameSite
server:
servlet:
session:
cookie:
same-site: lax| SameSite 值 | 行为 | 推荐场景 |
|---|---|---|
Strict | 完全禁止跨站携带 Cookie | 安全性最高,银行类应用 |
Lax | 允许 GET 方式的安全跨站请求携带 Cookie | 通用默认值 |
None | 不限制跨站携带,必须配合 Secure | OAuth 等第三方嵌入场景 |
CSRF Token:经典方案永不过时
对于传统服务端渲染的应用,CSRF Token 是最成熟可靠的防御方案。
后端生成和校验 Token:
// Spring Security 自动集成 CSRF Token
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
return http.build();
}
}前端在请求中携带 Token:
// 从 meta 标签或 Cookie 中获取 CSRF Token
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content;
// 在每个非 GET 请求中携带
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(transferData),
credentials: 'include'
});Referer/Origin 检查:辅助防线
服务端通过对 Origin 或 Referer 头部进行校验,拒绝来源不明的请求:
// Spring Security 配置 Referer 策略
http
.csrf(csrf -> csrf
.requireCsrfProtectionMatcher(request -> {
// 只对 POST/PUT/DELETE 方法做 CSRF 检查
if (!"POST".equals(request.getMethod()) &&
!"PUT".equals(request.getMethod()) &&
!"DELETE".equals(request.getMethod())) {
return false;
}
// 检查 Origin 是否在白名单内
String origin = request.getHeader("Origin");
if (origin != null) {
return !ALLOWED_ORIGINS.contains(origin);
}
return true;
})
);开发编码建议: CSRF 防御应当采用纵深策略,优先使用 SameSite Cookie(默认 Lax),再配合 CSRF Token 或自定义请求头作为增强。前后端分离架构(JWT 无状态认证)天然免疫 CSRF,但也需要确认没有使用 Cookie 传递认证凭证。
CSP 策略配置详解
理解 CSP 指令体系
CSP 策略由指令-来源对组成,每个指令控制一类资源的加载行为。
Content-Security-Policy: <指令> <来源>; <指令> <来源>核心指令速查表:
| 指令 | 控制范围 | 开发注意事项 |
|---|---|---|
default-src | 所有未显式设置指令的资源的兜底来源 | 建议始终设置此值 |
script-src | JavaScript 脚本来源 | 禁用 unsafe-inline,使用 nonce/hash |
style-src | CSS 样式来源 | 允许 unsafe-inline 但需评估风险 |
img-src | 图片来源 | 包含 data: 以支持 Base64 图片 |
connect-src | fetch/XMLHttpRequest/WebSocket 目标 | 严格限制 API 端点 |
frame-src | iframe 嵌入来源 | 不需要 iframe 时设为 none |
frame-ancestors | 控制自身页面能否被其他页面 iframe 嵌入 | 防点击劫持的关键指令 |
base-uri | <base> 标签允许的 URL | 限制可防止 base 标签劫持 |
form-action | 表单提交的目标 URL | 限制可防止表单提交到恶意地址 |
常见场景的 CSP 配置模板
场景一:纯静态站点(无外部资源):
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'场景二:使用 CDN 和第三方分析服务:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com https://www.googletagmanager.com;
style-src 'self' https://cdn.example.com;
img-src 'self' https://www.google-analytics.com;
connect-src 'self' https://api.example.com场景三:单页应用(SPA)需要动态加载脚本:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{随机值}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
base-uri 'self'开发者常见 CSP 配置错误
错误一:使用 unsafe-inline 且不配合 nonce——这会让所有内联脚本都可以执行,极大削弱 CSP 防护价值。
错误二:CSP 过于宽松——script-src 'self' 'unsafe-inline' https://* 几乎等同于没有 CSP。
错误三:忽略 base-uri——未限制 base-uri 时,攻击者可通过注入 <base> 标签篡改页面中所有相对路径的解析。
错误四:CSP 语法错误——指令名拼写错误或来源格式不正确,整条策略会被忽略。建议使用 CSP Evaluator 验证策略合法性。
点击劫持
攻击原理
攻击者通过透明 iframe 将目标页面覆盖在伪造的界面上,诱骗用户点击目标页面上的按钮或链接。
┌──────────────────────┐
│ 攻击者伪造页面 │
│ ┌────────────────┐ │
│ │ 透明 iframe │ │
│ │(隐藏的目标页面) │ │
│ │ [转账按钮] │ │ ← 用户实际点击此处
│ └────────────────┘ │
│ [点击领取优惠券] │ ← 用户以为自己点击这里
└──────────────────────┘X-Frame-Options:传统的防御方案
# Nginx 配置
add_header X-Frame-Options "SAMEORIGIN" always;
# 或 add_header X-Frame-Options "DENY" always;// Express 中间件
const helmet = require('helmet');
app.use(helmet.frameguard({ action: 'sameorigin' }));
// 或 app.use(helmet.frameguard({ action: 'deny' }));CSP frame-ancestors:更现代的选择
frame-ancestors 是 CSP 提供的替代方案,支持多域名白名单:
Content-Security-Policy: frame-ancestors 'self' https://trusted-app.example.com两者同时存在时,浏览器优先使用 frame-ancestors。推荐同时设置两者以兼容老旧浏览器。
Frameguard 脚本:留给用户的最后保险
Frameguard(或称 Frame Busting)是前端的兜底手段:
// Frameguard 脚本
(function() {
if (window.self !== window.top) {
try {
window.top.location.href = window.self.location.href;
} catch (e) {
// 某些浏览器沙箱会阻止 top.location 写入
document.body.innerHTML = `
<style>body { background: red; color: white; text-align: center; padding: 40px; }</style>
<h1>安全警告:此页面被非法嵌入</h1>
<p>请通过原始链接访问此页面</p>
`;
}
}
})();三段式防御方案: X-Frame-Options(兼容旧浏览器)+ CSP frame-ancestors(精准控制)+ Frameguard 脚本(前端兜底)。
中间人攻击
HTTPS:一切加密通信的基础
中间人攻击(MITM)的核心是攻击者拦截并篡改客户端与服务端之间的通信。HTTPS 通过 TLS 加密协议确保通信的机密性和完整性。
前端开发者需要关注的 HTTPS 实践:
// ❌ 危险:在前端代码中允许 HTTP 连接
fetch('http://api.example.com/data'); // 明文传输
// ✅ 安全:始终使用 HTTPS
fetch('https://api.example.com/data');
// ✅ 严格模式:禁止混合内容
// 在 CSP 中阻止混合内容加载
Content-Security-Policy: block-all-mixed-content**混合内容(Mixed Content)**是指 HTTPS 页面中加载了 HTTP 子资源(脚本、样式、图片等)。浏览器会警告甚至阻止这类请求:
<!-- ❌ 混合内容:HTTPS 页面加载 HTTP 脚本 -->
<script src="http://cdn.example.com/script.js"></script>
<!-- ✅ 安全:统一使用 HTTPS -->
<script src="https://cdn.example.com/script.js"></script>HSTS:强制 HTTPS 的通行证
HSTS(HTTP Strict-Transport-Security)告诉浏览器:未来一段时间内,只能通过 HTTPS 访问该域名。这可以有效防止 SSL Stripping 攻击。
# Nginx HSTS 配置
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;// Express 使用 helmet 配置 HSTS
const helmet = require('helmet');
app.use(helmet.hsts({
maxAge: 31536000, // 1 年
includeSubDomains: true,
preload: true
}));HSTS preload 注意事项: 提交到浏览器预加载列表后,域名将无法撤销,务必确认所有子域名都已支持 HTTPS。
证书固定与 Expect-CT
对于安全要求极高的应用,可以使用证书固定(Certificate Pinning)或 Expect-CT 头部防止伪造证书:
Expect-CT: max-age=86400, enforce, report-uri="https://example.com/report"开发者注意事项: 不建议在前端硬编码证书指纹(Certificate Pinning),因为证书更新时会导致服务不可用。推荐使用 Expect-CT 头部或运维侧配置。
前端供应链安全
依赖漏洞管理
npm 生态的开放性带来了便利也带来了风险。恶意包(如 event-stream 事件)和已知漏洞(如 lodash 原型污染)是前端供应链安全的最大威胁。
# 日常安全操作
npm audit --audit-level=high # 审查高危漏洞
npm outdated # 检查过时依赖
npm audit fix # 修复可自动修复的漏洞package.json 中的安全实践:
{
"scripts": {
"preinstall": "npx npm-audit-resolver",
"audit": "npm audit --audit-level=high",
"snyk": "snyk test"
},
"overrides": {
"semver": "7.5.4",
"minimatch": "9.0.3",
"word-wrap": "1.2.4"
}
}SRI:确保 CDN 资源未被篡改
子资源完整性(SRI)通过哈希值校验确保 CDN 加载的第三方资源未被篡改:
<!-- 使用 SRI 加载第三方库 -->
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM"
crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.0/dist/vue.global.prod.js"
integrity="sha384-ABC123DEF456GHI789JKL012MNO345PQR678STU890VWX"
crossorigin="anonymous"></script>生成 SRI 哈希值:
# 使用 OpenSSL 生成
openssl dgst -sha384 -binary vendor.js | openssl base64 -A
# 在线工具
# https://www.srihash.org/SRI 使用要点:
- 始终包含
crossorigin="anonymous"属性 - CDN 必须支持 CORS 请求
- 资源更新后同步更新
integrity值 - 在 CI/CD 流程中自动计算 SRI 哈希
开发者供应链安全行为规范
- 审查
package.json:引入新依赖前评估其维护状态、下载量、安全记录 - 锁定版本:提交
package-lock.json或yarn.lock到版本控制 - 优先使用官方源:配置 npm registry 为官方源或私有仓库
- 避免使用已废弃的包:使用
npm deprecate检查,或用npm doctor诊断 - 使用工具辅助安全审计:集成 Snyk、Socket.dev 等工具到 CI/CD
安全 HTTP 头部总结表格
以下表格从前端配置修改的角度,汇总了各安全头部的核心作用与推荐配置值:
| HTTP 安全头部 | 作用 | 推荐配置值 | 前端配置方式 |
|---|---|---|---|
Content-Security-Policy | 限制资源加载来源,防御 XSS | default-src 'self'; script-src 'self' | 服务端响应头或 <meta http-equiv> |
X-Content-Type-Options | 禁止 MIME 嗅探 | nosniff | 服务端配置 |
X-Frame-Options | 防御点击劫持 | DENY 或 SAMEORIGIN | 服务端配置 |
Strict-Transport-Security | 强制 HTTPS 连接 | max-age=31536000; includeSubDomains | 服务端配置 |
Referrer-Policy | 控制 Referer 头部信息量 | strict-origin-when-cross-origin | <meta name="referrer"> 或服务端 |
Permissions-Policy | 控制浏览器 API 权限 | camera=(), microphone=(), geolocation=() | <meta http-equiv> 或服务端 |
Set-Cookie: SameSite | 防御 CSRF | SameSite=Lax; Secure; HttpOnly | 服务端配置 |
Expect-CT | 证书透明度检查 | max-age=86400, enforce | 服务端配置 |
前端可以通过 <meta> 标签设置部分安全头(但推荐在服务端配置):
<!-- CSP 可以通过 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'">
<!-- Referrer-Policy 可以通过 meta 标签设置 -->
<meta name="referrer" content="strict-origin-when-cross-origin">注意
通过 <meta> 标签设置的安全头部优先级低于服务端响应头,且不支持某些头部(如 X-Frame-Options、Strict-Transport-Security)。生产环境强烈推荐在服务端或反向代理层配置安全头部。
总结:前端安全编码思维
安全不是功能,而是代码的一种属性。养成安全编码的思维方式比掌握任何具体技术都更重要:
| 思维方式 | 具体体现 |
|---|---|
| 不信任输入 | 所有用户输入、URL 参数、API 响应、第三方数据都可能是恶意的 |
| 纵深防御 | 不依赖单一安全机制,XSS + CSP、HTTPS + HSTS、CSRF Token + SameSite |
| 最小权限 | JS API 权限最小化、CSP 来源最小化、依赖引入最小化 |
| 安全滞后 | 任何安全机制在浏览器中都是"先放行后阻止"的,需结合服务端验证 |
| 持续审视 | 每次代码改动都问自己:这个改动是否引入了安全风险? |