跳到主要内容

OWASP Top 10 详解

OWASP(开放式 Web 应用安全项目)Top 10 是评估 Web 应用安全风险的公认标准。它基于全球安全数据总结了最严重的安全威胁,为开发者和安全从业者提供防御指南。本文档基于 OWASP Top 10:2021 版本。

为什么 OWASP Top 10 如此重要?

OWASP Top 10 为开发者和组织提供了:

  • 通用语言:统一的安全风险命名和分类
  • 优先级指导:帮助团队确定修复顺序
  • 意识提升:让安全成为开发过程的一部分
  • 合规参考:许多安全标准和法规引用 OWASP Top 10
版本演进

OWASP Top 10 每隔几年更新一次。2021 版相比 2017 版有重要变化:

  • 新增"不安全设计"和"服务器端请求伪造"类别
  • "敏感数据泄露"更名为"加密失败"
  • "XML 外部实体"合并到"安全错误配置"

A01:2021 - 失效的访问控制(Broken Access Control)

访问控制决定了用户对资源的操作边界。失效的访问控制是最常见也是最危险的安全风险,连续多年位居榜首。

常见问题

垂直越权(权限提升):普通用户访问管理员功能

// 危险:仅前端隐藏按钮,后端无验证
app.delete('/api/admin/users/:id', (req, res) => {
// 缺少角色验证!
deleteUser(req.params.id);
});

// 攻击者直接调用 API
fetch('/api/admin/users/123', { method: 'DELETE' });

水平越权(数据泄露):通过修改参数访问他人数据

// 危险:直接使用用户提供的 ID
app.get('/api/orders/:id', (req, res) => {
const order = db.getOrder(req.params.id);
res.json(order); // 未验证是否属于当前用户
});

// 攻击者遍历 ID 获取所有订单
fetch('/api/orders/1'); // 别人的订单
fetch('/api/orders/2'); // 另一个人的订单

其他常见问题

  • 浏览目录列表泄露敏感文件
  • API 缺少速率限制导致暴力破解
  • JWT 令牌未验证或弱签名
  • 会话固定攻击

防护措施

核心原则:默认拒绝,显式授权

// 正确:后端验证权限
app.delete('/api/admin/users/:id', authenticate, requireAdmin, (req, res) => {
deleteUser(req.params.id);
});

// 正确:验证资源所有权
app.get('/api/orders/:id', authenticate, (req, res) => {
const order = db.getOrder(req.params.id);
if (order.userId !== req.user.id) {
return res.status(403).json({ error: '无权访问' });
}
res.json(order);
});

// 中间件示例
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') {
return res.status(403).json({ error: '需要管理员权限' });
}
next();
}

访问控制清单

检查项说明
权限验证每个敏感操作都验证用户权限
资源归属确保用户只能访问自己的数据
速率限制防止暴力破解和滥用
会话管理登出后立即失效令牌
日志记录记录失败的访问尝试

A02:2021 - 加密失败(Cryptographic Failures)

原名"敏感数据泄露",2021 版更名为"加密失败",强调这是实现问题而非结果问题。

常见问题

使用弱哈希算法存储密码

// 极其危险:MD5 可被彩虹表破解
const hash = md5(password);

// 危险:SHA-256 无盐可被彩虹表破解
const hash = sha256(password);

// 仍然危险:简单加盐不够
const hash = sha256(password + 'fixed_salt');

传输层加密不足

// 危险:允许 HTTP 访问
app.listen(80);

// 危险:Cookie 未设置 Secure 标志
res.cookie('session', token); // HTTP 下也会发送

防护措施

密码存储:使用专用哈希算法

// 正确:使用 bcrypt
const bcrypt = require('bcrypt');
const hashedPassword = await bcrypt.hash(password, 12); // cost factor 12

// 验证密码
const isValid = await bcrypt.compare(password, hashedPassword);
# Python 示例
import bcrypt

hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
isValid = bcrypt.checkpw(password.encode(), hashed)

推荐算法对比

算法适用场景特点
Argon2密码存储(首选)抗 GPU 破解,内存困难
bcrypt密码存储成本因子可调,广泛支持
scrypt密码存储内存困难,抗 ASIC
PBKDF2密码存储(备选)标准算法,兼容性好
AES-256-GCM数据加密对称加密,需安全存储密钥
RSA-2048+密钥交换非对称加密

传输安全

// 强制 HTTPS
app.use((req, res, next) => {
if (!req.secure && req.get('x-forwarded-proto') !== 'https') {
return res.redirect(`https://${req.get('host')}${req.url}`);
}
next();
});

// 安全 Cookie 设置
res.cookie('session', token, {
httpOnly: true, // 防止 XSS 读取
secure: true, // 仅 HTTPS 传输
sameSite: 'strict', // 防止 CSRF
maxAge: 3600000 // 1 小时过期
});

A03:2021 - 注入(Injection)

注入漏洞是最古老却依然普遍的安全风险,包括 SQL 注入、命令注入、LDAP 注入等。

SQL 注入

当用户输入被拼接到 SQL 语句中执行时,就会产生注入漏洞。

// 危险:直接拼接 SQL
const query = `SELECT * FROM users WHERE id = ${userId}`;
// 攻击输入: 1 OR 1=1
// 实际执行: SELECT * FROM users WHERE id = 1 OR 1=1

防护:参数化查询

// 正确:使用占位符
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [userId]);

详见 SQL 注入专题

命令注入

用户输入被拼接到系统命令中执行。

// 危险:直接拼接命令
const { exec } = require('child_process');
exec(`ping ${host}`, (err, stdout) => {
// 攻击输入: ; rm -rf /
// 实际执行: ping ; rm -rf /
});

防护:避免执行 shell 命令

// 正确:使用原生模块
const dns = require('dns');
dns.lookup(host, (err, address) => {
// 安全,不会执行任意命令
});

// 如果必须执行命令,使用白名单验证
const allowedHosts = ['google.com', 'github.com'];
if (!allowedHosts.includes(host)) {
throw new Error('Invalid host');
}

LDAP 注入

// 危险:LDAP 查询拼接
const filter = `(uid=${username})`;
// 攻击输入: *)(uid=*))(|(uid=*
// 实际执行: (uid=*)(uid=*))(|(uid=*) -- 永远返回真

防护:参数化 LDAP 查询

// 正确:转义特殊字符
function escapeLDAP(input) {
return input.replace(/[()*\\\x00-\x1f]/g, (char) => {
return '\\' + char.charCodeAt(0).toString(16).padStart(2, '0');
});
}

A04:2021 - 不安全设计(Insecure Design)

这是 2021 版新增的类别,强调安全问题往往源于设计阶段的缺陷。

什么是安全设计缺陷?

与实现漏洞不同,设计缺陷是系统架构层面的问题,即使代码完美实现,系统仍然不安全。

案例:电影票预订系统

// 设计缺陷:用户可以选择任意座位数量
app.post('/api/book', async (req, res) => {
const { seats } = req.body; // 用户指定数量
await bookingService.book(seats);
// 问题:没有限制最大数量,可能被滥用
});

改进设计

// 正确:业务规则约束
const MAX_SEATS_PER_BOOKING = 10;

app.post('/api/book', async (req, res) => {
const { seats } = req.body;

if (seats.length > MAX_SEATS_PER_BOOKING) {
return res.status(400).json({ error: '每次最多预订 10 个座位' });
}

// 原子操作,防止超卖
const success = await bookingService.bookAtomically(seats);
if (!success) {
return res.status(409).json({ error: '座位已被占用' });
}
});

威胁建模

安全设计需要在开发前进行威胁建模:

  1. 识别资产:什么需要保护?(用户数据、支付信息、知识产权)
  2. 识别威胁:谁可能攻击?如何攻击?
  3. 评估风险:攻击的可能性和影响
  4. 设计对策:如何在架构层面防御

安全设计原则

原则说明示例
最小权限只授予必要的权限普通用户不能访问管理接口
纵深防御多层安全控制参数验证 + 参数化查询 + 数据库权限限制
默认安全默认拒绝访问新功能默认关闭,需主动授权
失败安全失败时保持安全状态认证失败不泄露用户是否存在

A05:2021 - 安全错误配置(Security Misconfiguration)

这是最常见的安全问题之一,包括使用默认配置、暴露错误信息、不必要的功能等。

常见问题

默认凭据

# 危险:生产环境使用默认密码
database:
host: localhost
user: admin
password: admin # 默认密码!

错误信息泄露

// 危险:向用户暴露详细错误
app.use((err, req, res, next) => {
res.status(500).json({
error: err.message,
stack: err.stack, // 泄露服务器路径和代码结构
query: req.query // 泄露请求参数
});
});

不必要的功能

# 危险:生产环境开启调试端点
management:
endpoints:
web:
exposure:
include: "*" # 暴露所有管理端点

防护措施

安全配置清单

// 安全的错误处理
app.use((err, req, res, next) => {
console.error(err); // 服务端记录详细日志

// 客户端只返回通用信息
res.status(500).json({
error: '服务器内部错误',
requestId: req.id // 用于追踪
});
});

// 安全的 HTTP 头
app.use((req, res, next) => {
res.removeHeader('X-Powered-By'); // 隐藏技术栈
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});

环境隔离

# 开发环境
debug: true
detailed_errors: true

# 生产环境
debug: false
detailed_errors: false
logging: warning

A06:2021 - 易受攻击和过时的组件(Vulnerable and Outdated Components)

现代应用依赖大量第三方组件,组件漏洞成为重要攻击面。

风险来源

  • 使用有已知漏洞的库版本
  • 未及时更新依赖
  • 不必要的依赖增加了攻击面
  • 依赖的依赖(传递依赖)也有漏洞

防护措施

建立软件物料清单(SBOM)

# 生成依赖树
npm list --all

# 检查已知漏洞
npm audit

# 使用 Snyk 扫描
snyk test

依赖管理策略

  1. 最小化依赖:只安装必要的包
  2. 定期更新:至少每月检查更新
  3. 锁定版本:使用 lock 文件确保可重复构建
  4. 自动扫描:在 CI/CD 中集成安全扫描
# GitHub Actions 自动依赖扫描
name: Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm audit --audit-level=high
- uses: snyk/actions/node@master

A07:2021 - 识别和身份验证失败(Identification and Authentication Failures)

身份验证是安全的第一道防线,验证失败可能导致账户被盗用。

常见问题

允许暴力破解

// 危险:无限制的登录尝试
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);
if (user) {
res.json({ token: generateToken(user) });
} else {
res.status(401).json({ error: '用户名或密码错误' });
}
});

弱密码策略

// 危险:允许弱密码
if (password.length >= 4) { // 只检查长度
// 接受密码如 "1234"
}

防护措施

防暴力破解

// 正确:登录限制
const loginAttempts = new Map();

app.post('/login', async (req, res) => {
const { username, password } = req.body;

// 检查尝试次数
const attempts = loginAttempts.get(username) || 0;
if (attempts >= 5) {
return res.status(429).json({
error: '尝试次数过多,请 15 分钟后重试'
});
}

const user = await authenticate(username, password);
if (user) {
loginAttempts.delete(username);
res.json({ token: generateToken(user) });
} else {
loginAttempts.set(username, attempts + 1);
res.status(401).json({ error: '用户名或密码错误' });
}
});

强密码策略

// 正确:全面的密码验证
function validatePassword(password) {
const minLength = 12;
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[!@#$%^&*]/.test(password);
const notCommon = !commonPasswords.includes(password.toLowerCase());

return password.length >= minLength
&& hasUpper && hasLower
&& hasNumber && hasSpecial
&& notCommon;
}

多因素认证(MFA)

// TOTP 双因素认证示例
const speakeasy = require('speakeasy');

// 生成密钥
const secret = speakeasy.generateSecret({
name: 'MyApp ([email protected])'
});

// 验证 TOTP
const isValid = speakeasy.totp.verify({
secret: secret.base32,
encoding: 'base32',
token: userProvidedCode
});

A08:2021 - 软件和数据完整性失败(Software and Data Integrity Failures)

不验证软件或数据的来源和完整性就使用,可能导致供应链攻击。

常见问题

不安全的反序列化

// 危险:反序列化不可信数据
ObjectInputStream ois = new ObjectInputStream(inputStream);
Object obj = ois.readObject(); // 可能执行任意代码

不验证依赖完整性

# 危险:直接安装未验证的包
npm install some-package # 可能被篡改

防护措施

安全反序列化

// 正确:使用白名单限制可反序列化的类
public class SafeObjectInputStream extends ObjectInputStream {
private static final Set<String> ALLOWED_CLASSES = Set.of(
"com.example.User",
"com.example.Order"
);

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) {
if (!ALLOWED_CLASSES.contains(desc.getName())) {
throw new InvalidClassException("Unauthorized deserialization", desc.getName());
}
return super.resolveClass(desc);
}
}

验证依赖完整性

# 使用 lock 文件确保一致性
npm ci # 而非 npm install

# 验证签名
npm audit signatures

CI/CD 安全

# 使用签名验证
- name: Verify artifact signature
run: |
cosign verify --key ./pub.key registry.example.com/app:v1.0

# 限制 CI 权限
permissions:
contents: read
packages: write

A09:2021 - 安全日志和监控失败(Security Logging and Monitoring Failures)

没有足够的日志和监控,无法及时发现和响应攻击。

应记录的安全事件

事件类型示例
认证事件登录成功/失败、密码重置、MFA 验证
授权事件权限提升、访问拒绝
敏感操作数据导出、配置变更、删除操作
异常行为速率限制触发、异常 IP 访问

实现示例

// 安全日志记录
const securityLog = {
log: (event, details) => {
const entry = {
timestamp: new Date().toISOString(),
event,
userId: details.userId,
ip: details.ip,
userAgent: details.userAgent,
details: details.data
};

// 发送到安全日志系统
securityLogger.log(entry);

// 敏感事件触发告警
if (['LOGIN_FAILURE', 'PERMISSION_DENIED', 'RATE_LIMITED'].includes(event)) {
alertSystem.notify(entry);
}
}
};

// 使用示例
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);

if (user) {
securityLog.log('LOGIN_SUCCESS', {
userId: user.id,
ip: req.ip,
userAgent: req.headers['user-agent']
});
res.json({ token: generateToken(user) });
} else {
securityLog.log('LOGIN_FAILURE', {
userId: username, // 尝试登录的用户名
ip: req.ip,
userAgent: req.headers['user-agent'],
data: { reason: 'INVALID_CREDENTIALS' }
});
res.status(401).json({ error: '认证失败' });
}
});

日志安全注意事项

  • 不记录敏感数据(密码、令牌、PII)
  • 保护日志不被篡改
  • 设置合理的保留期限
  • 集中存储和分析

A10:2021 - 服务器端请求伪造(SSRF)

应用在未经验证的情况下根据用户输入发起网络请求,攻击者可借此探测内网或访问内部服务。

攻击场景

// 危险:未验证的 URL 请求
app.get('/fetch', async (req, res) => {
const { url } = req.query;
const response = await fetch(url); // 用户控制 URL
res.send(await response.text());
});

// 攻击者请求
// /fetch?url=http://169.254.169.254/latest/meta-data/
// 访问云服务元数据获取敏感信息

防护措施

URL 白名单验证

// 正确:验证 URL
const allowedDomains = ['api.github.com', 'api.twitter.com'];
const blockedProtocols = ['file://', 'gopher://', 'dict://'];

function validateUrl(urlString) {
try {
const url = new URL(urlString);

// 检查协议
if (blockedProtocols.some(p => urlString.toLowerCase().startsWith(p))) {
return { valid: false, reason: '不允许的协议' };
}

// 只允许 HTTP/HTTPS
if (!['http:', 'https:'].includes(url.protocol)) {
return { valid: false, reason: '仅允许 HTTP/HTTPS' };
}

// 检查域名白名单
if (!allowedDomains.includes(url.hostname)) {
return { valid: false, reason: '域名不在白名单中' };
}

// 解析 IP 防止绕过
const { lookup } = require('dns').promises;
const addresses = await lookup(url.hostname);
const ip = addresses.address;

// 检查是否为内网 IP
if (isPrivateIP(ip)) {
return { valid: false, reason: '禁止访问内网地址' };
}

return { valid: true, url };
} catch (e) {
return { valid: false, reason: '无效的 URL' };
}
}

总结

OWASP Top 10 2021 总结了 Web 应用最常见的安全风险。理解这些漏洞并采取相应的防护措施是构建安全 Web 应用的基础。

安全风险趋势

  1. 访问控制问题持续高发:授权逻辑复杂,容易遗漏
  2. 注入攻击依然普遍:防护知识已成熟,但实现仍有漏洞
  3. 设计安全成为新重点:设计阶段的缺陷难以通过代码修复
  4. 供应链安全日益重要:第三方组件成为重要攻击面

系统性防护策略

需求阶段 → 安全需求分析
设计阶段 → 威胁建模
开发阶段 → 安全编码规范、代码审查
测试阶段 → 安全测试(SAST/DAST/渗透测试)
部署阶段 → 安全配置、WAF
运维阶段 → 监控、日志、应急响应

练习

  1. 使用 OWASP ZAP 或 Burp Suite 对测试应用进行安全扫描
  2. 审查一个真实项目的访问控制逻辑
  3. 为你的项目建立依赖安全扫描流程
  4. 实现一个包含速率限制和多因素认证的登录系统

参考资料