搭个靶场熟悉OWASPTop10,感觉好多岗位要求要熟悉这个
首先对这个东西做个介绍(2025版)
A01 失效的访问控制 — 连续两届排名第一;在官方数据里,100% 的受测应用都存在这类问题
本质:已经确认了“你是谁”,却没有管住“你能做什么”
典型:修改 URL 里的 ?id=1002 就能查看别人的订单;前端把管理按钮隐藏了,但后端没有再次校验;修改 JWT 里的 role 就变成了管理员;force browsing,也就是直接访问 /admin_xxx
我的项目:CVE-2021-41773 的路径穿越就是 CWE-22,正好在 A01 映射的 40 个 CWE 里。2025 版把 SSRF(2021 版的 A10)也合并进来了,原因是二者本质相同:服务端替攻击者访问了不该访问的东西
防:每个请求都在服务端重新判定权限,默认拒绝
A02 安全配置错误 — 从第 5 位升到第 2 位
本质:代码没写错,是“开着的东西”错了
典型:debug 模式开着、目录列表可浏览、默认口令、错误页回显版本号、云存储桶公开
我的项目:你在复现路径穿越时手写的那行 Alias 和 CGI 配置,本身就属于 A02。CVE 往往先有一个配置前提,再有一个代码缺陷
防:配置基线化,关掉一切用不上的模块
A03 软件供应链失效 — 新增,在社区调查中得了 50% 的第一名票
本质:你信任的依赖、构建或分发环节被做了手脚
典型:SolarWinds、Log4Shell,以及 2025 年 npm 上自我复制的蠕虫 Shai-Hulud(在 500+ 个包版本中传播)
注意:这类问题在数据里出现率最高(平均 5.19%),但相关 CVE 极少,因为测不出来
防:生成 SBOM、锁定版本、只从官方源安装、校验签名
A04 加密机制失效 — 从第 2 位降到第 4 位
本质:该加密的地方用了明文,或者用了错误的算法和模式
典型:密码明文存储或用 MD5 存储、HTTP 传输密码、密钥硬编码进代码、用 AES-ECB
防:用 bcrypt 或 argon2 存密码,使用 AES-GCM,强制 TLS,密钥外置
A05 注入 — 从第 3 位降到第 5 位,但仍是 CVE 数量最多的类别
本质:数据被拼进代码里执行了
典型:SQLi、XSS、命令注入、SSTI、XXE
我的项目:waf-lab 挡的主要就是这一类。另外,CVE-2021-41773 如果启用了 mod_cgi,就会从路径穿越升级成命令执行,于是从 A01 跨到 A05
防:参数化查询、输出编码、绝不拼接 shell
A06 不安全设计 — 从第 4 位降到第 6 位
本质:代码没有 bug,是业务规则本身可以被滥用
典型:找回密码靠密保问题、优惠券可以无限叠加、没有限速导致可以直接爆破
防:威胁建模(STRIDE)、设计评审、写“滥用用例”
A07 认证失效 — 保持第 7 位,改名(去掉了 “Identification”)
本质:确认“你是谁”这一步做错了
典型:弱口令且无锁定、会话 ID 可预测、登出后 session 仍然有效、JWT 永不过期
防:MFA、使用成熟认证框架、限速、在服务端让会话失效
A08 软件或数据完整性失效 — 保持第 8 位
本质:东西被改过,但你没验证
典型:原生反序列化(Java、PHP unserialize)、CDN 脚本没做 SRI、CI 产物可以被替换
防:校验签名、禁用原生反序列化、使用 SRI
A09 日志与告警失效 — 保持第 9 位,改名(Monitoring → Alerting)
本质:被攻击了没记录,或者记录了但没人被叫醒
我的项目:waf-lab 的 logs/*.jsonl 就是这一类。改名的重点恰恰是“告警”:只记录不触发告警等于没记录
防:记录安全事件而不只是记录请求、集中收集日志、设置阈值告警
A10 异常处理不当 — 新增,包含 24 个 CWE
本质:出错时反而“松手”了
典型:catch 之后直接放行(fail open)、报错页把堆栈和 SQL 语句回显出去、事务中断不回滚
防:默认 fail closed(出错就拒绝)、统一错误页、全局兜底、幂等回滚
简单来说
写错代码那一半:
- A01 失效的访问控制 —— 身份认了,但”能干什么”没管住
- A04 加密机制失效 —— 该加密的用了明文,或者用了错的算法
- A05 注入 —— 数据被拼进代码里执行了
- A07 认证失效 —— “你是谁”这一步做错了
- A10 异常处理不当 —— 出错时反而松手了
配错系统那一半:
- A02 安全配置错误 —— 代码没错,开着的东西错了
- A03 软件供应链失效 —— 你信任的依赖被动了手脚
- A06 不安全设计 —— 代码没 bug,业务规则本身能被滥用
- A08 完整性失效 —— 东西被改了,但你没验证
- A09 日志与告警失效 —— 被打了没记录,或者记了没人管
换到软件生命周期上看更清楚:
- 需求与设计阶段 → A06
- 编码实现阶段 → A01、A04、A05、A07、A10
- 拉依赖与构建阶段 → A03、A08
- 部署与配置阶段 → A02
- 上线与运维阶段 → A09
A01 失效的访问控制
本质:系统知道你是谁了,但没管住你能碰什么。
长什么样
- 改 URL 里的
?id=1002,就能看到别人的订单(越权 / IDOR) - 管理按钮在前端用 JS 藏了,后端接口没校验,直接
curl就进去了 - JWT 里的
role字段是明文的,改一下变管理员 - 目录能直接浏览,
.git、备份文件躺在 web 根目录里
跟自己项目的交集
我复现过的 CVE-2021-41773 就是 CWE-22 路径穿越,正好在 A01 映射的 40 个 CWE 清单里。当时只把它当”Apache 的一个洞”,现在看它同时踩了三条:
- A01:越权访问了 web 根目录之外的文件
- A02:前提是配置里开了
Alias和Require all granted - A05:如果启用了 mod_cgi,路径穿越直接升级成命令执行
一个漏洞落在多个类别里,这就是官方说的”必然重叠”。
练习:DVWA → File Inclusion,把 ?page=../../../../etc/passwd 打进去,再把 page 换成绝对路径 /etc/passwd 对比。两层校验漏一层就穿。
怎么防
- 每次请求都在服务端重新判定,默认拒绝
- 用”记录归属”而不是”用户能传 id”来建模权限
- 关掉目录列表,web 根目录不留
.git和备份文件
A02 安全配置错误
本质:代码没写错,是”开着的东西”错了。
长什么样
我写了个 30 行的 PHP 来演示最常见的形态 —— 生产环境忘了关 debug:
<?php$debug = true; // ← 漏洞开关
$config = [ 'db_host' => 'db', 'db_user' => 'dvwa', 'db_pass' => 'p@ssw0rd', // 硬编码口令 'secret' => '7f2c1e9a4b',];
$date = $_GET['date'] ?? 'today';$path = '/var/log/app/access-' . $date . '.log';$log = @file_get_contents($path);
header('Content-Type: text/plain; charset=utf-8');
if ($log === false) { http_response_code(500); if ($debug) { echo "[DEBUG] file_get_contents() failed\n"; echo "target : {$path}\n"; echo "cwd : " . getcwd() . "\n"; echo "php : " . PHP_VERSION . "\n"; echo "server : " . ($_SERVER['SERVER_SOFTWARE'] ?? '') . "\n"; echo "config : " . json_encode($config, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n"; } else { echo "服务繁忙,请稍后再试\n"; } exit;}正常访问一次,故意让它读一个不存在的日志(实测输出):
[DEBUG] file_get_contents() failedtarget : /var/log/app/access-today.logcwd : /var/www/html/vulnphp : 8.5.11server : Apache/2.4.68 (Debian)config : { "db_host": "db", "db_user": "dvwa", "db_pass": "p@ssw0rd", "secret": "7f2c1e9a4b"}
[HTTP 500]一次失败的请求,泄露了:绝对路径、PHP 版本、Apache 版本、数据库地址、数据库口令、密钥。这就是攻击者做侦察时最想要的东西,而且不用任何技术。
把 $debug 改成 false,同样的请求返回”服务繁忙,请稍后再试”。差别只有一行。
A02 的另一半:目录能浏览 + 备份文件没删
我建了个 backup/ 目录,里面放 db.sql.bak,先访问:
curl -i http://127.0.0.1:8000/backup/HTTP/1.1 403 ForbiddenDate: Thu, 22 Oct 2026 03:42:22 GMTServer: Apache/2.4.68 (Debian)Content-Length: 316Content-Type: text/html; charset=iso-8859-1403 说明 Apache 配置里关了目录索引(Options -Indexes),这是对的。现在手动开一下 —— 在目录里丢一个 .htaccess:
Options +Indexes再访问,实测返回:
<title>Index of /backup</title>...<tr><td><a href="db.sql.bak">db.sql.bak</a></td><td align="right">439 </td></tr>目录一览无余。一行配置,从 403 变 200,从”猜文件名”变成”文件列表任选”。
下下来看内容:
INSERT INTO users VALUES (1, 'admin', '5f4dcc3b5aa765d61d8327deb882cf99', 'admin'), (2, 'gordon', 'e10adc3949ba59abbe56e057f20f883e', 'user');这就接到 A04 了。
练习:就是用上面这个。先把 Options -Indexes 打开验证 403,再改回来验证 200,最后删掉 .htaccess 和备份文件。每一步都记录状态码,你会发现”安全”和”不安全”的差别只有一行。
怎么防
- 关目录列表:
Options -Indexes - 生产环境
display_errors = Off,APP_DEBUG=false - 备份文件不放在 web 根目录(
/var/backups/而不是public/) - 关掉用不到的模块和功能
A03 软件供应链失效
新增类别,社区调查里 50% 的人把它投成第一。映射的 CWE 只有 5~6 个,数据里的平均出现率却是十类最高。
本质:你信任的依赖、构建工具、分发渠道被动了手脚。
这一条最反直觉的地方在于:你没写错任何代码,也可能中招。 官方举的例子是 2019 年 SolarWinds(约 18000 家机构被波及)、Log4Shell,以及 2025 年那个自我复制的 npm 蠕虫 Shai-Hulud(感染了 500 多个包版本,专门偷开发机的 npm token,然后用偷到的 token 继续往外推毒版本)。
长什么样
我对自己的博客项目跑了一遍依赖审计:
cd LYH-Blogspnpm audit结果(实测):
122 vulnerabilities foundSeverity: 12 low | 45 moderate | 63 high | 2 critical两条 critical 的详情:
critical Astro: Remote code execution through AVIF image optimizationPackage astroVulnerable versions <7.2.8Patched versions >=7.2.8Paths . > @astrojs/mdx@5.0.6 > astro@6.3.5 . > astro@6.3.5
critical node-tar: Decompression/parse DoS via unlimited inputPackage tarVulnerable versions <=7.5.18Patched versions >=7.5.19Paths . > astro-icon@1.1.5 > @iconify/tools@4.2.0 > tar@7.5.16两条都很说明问题:
- astro 是我博客的框架本身,当前跑 6.3.5,而修复版本是 7.2.8 —— 差了一个大版本。这个漏洞是 AVIF 图片优化路径上的 RCE,也就是说只要我用到了那个功能,攻击者上传一张精心构造的图片就可能执行代码。
- tar 是被传递依赖带进来的:我根本没直接装它,是
astro-icon→@iconify/tools→tar一路拖进来的。这就是”传递依赖”最讨厌的地方 —— 你不知道它在,但它在。
怎么防
- 生成 SBOM(软件物料清单),知道你到底装了什么
- 锁版本:
package-lock.json/pnpm-lock.yaml/requirements.txt都要提交 - 不写
:latest—— 我那个docker images里一堆latest标签,这等于”我不知道跑的是哪个版本” - 只从官方源安装,优先选有签名的包
- 每周跑一次 audit,别等出事
A04 加密机制失效
本质:该加密的用了明文,或者加密方式本身就是错的。
长什么样
接上面 A02 拿到的那份备份文件。两个看起来很像密码的东西:
5f4dcc3b5aa765d61d8327deb882cf99e10adc3949ba59abbe56e057f20f883eMD5 值。既然是 MD5,就直接算一下常见口令(实测,用 macOS 自带的 md5 命令):
for w in password 123456 admin; do printf '%-10s -> %s\n' "$w" "$(printf '%s' "$w" | md5)"donepassword -> 5f4dcc3b5aa765d61d8327deb882cf99123456 -> e10adc3949ba59abbe56e057f20f883eadmin -> 21232f297a57a5a743894a0e4a801fc3两个哈希全部命中。admin 的密码是 password,gordon 的密码是 123456。
这里的关键不是”MD5 不安全”,而是”无盐的快速哈希根本不能用来存密码”。 MD5 设计目标是快,一秒能算几百万次,正好方便暴力破解。即使换成 SHA-256,问题一样 —— 快就是问题。
正确的做法是专门为存密码设计的慢哈希:
// 错的$hash = md5($password);
// 对的$hash = password_hash($password, PASSWORD_DEFAULT); // bcryptvar_dump(password_verify('123456', $hash)); // bool(true)password_hash 每次生成的结果都不一样(自动加盐),验证用 password_verify。它是故意慢的,每秒只能算几百次,让暴力破解的成本抬上去。
练习:拿一份自己造的”泄露数据”,先 MD5 一遍再破,然后换成 password_hash 再破一次。同一条命令,成功率从 100% 掉到 0,比任何解释都有说服力。
怎么防
- 存密码用 bcrypt / argon2,不用 MD5 / SHA 系列
- 传输强制 TLS,不在 HTTP 上传密码
- 密钥不进代码、不进镜像,用环境变量或密钥管理服务
- 对称加密用 AES-GCM 这类带认证的模式,不用 ECB
A05 注入
本质:数据被拼进了代码里执行。
长什么样
DVWA 的 SQL Injection 页面,low 级别,输入框里原样拼 SQL。打一条最经典的恒真条件:
# 直接打靶场(无 WAF)curl -s -o /dev/null -w 'HTTP %{http_code}\n' \ "http://127.0.0.1:8888/vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submit"HTTP 302302 是因为没带登录 cookie,被弹回登录页了。但注意:应用已经处理了这个请求,SQL 也拼进去执行了,只是结果没返回给我。响应码干净不代表没被打到。
现在把同一个请求指向我自己写的 WAF(block 模式):
curl -s -o /dev/null -w 'HTTP %{http_code}\n' \ "http://127.0.0.1:8889/vulnerabilities/sqli/?id=1%27%20OR%20%271%27%3D%271&Submit=Submit"HTTP 403WAF 的审计日志里记着这次判决:
score : 100 / 阈值 100action : block resp_status: 403backend_status: None (None = 请求没有走到后端)命中 : 1021 SQL 注入 · 恒真条件 +55; 1024 SQL 注入 · 引号闭合特征 +45backend_status: None 说明请求根本没转发到 DVWA,在 WAF 这一层就被拦了。这是 WAF 和”事后检测”的本质区别。
但这里有个必须说清的事:WAF 拦住了这条载荷,不代表 DVWA 的 SQL 注入被修好了。换一种编码、换一个参数位置、或者从 POST body 进,WAF 的规则就可能漏。 真正修 SQL 注入只有一条路:
// 错的:拼字符串$sql = "SELECT * FROM users WHERE id = '$id'";
// 对的:参数化查询$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");$stmt->execute([$id]);参数化查询把”数据”和”语句”彻底分开,数据库先编译语句结构,再绑定数据 —— 结构里没有可注入的位置。这是根治,WAF 是缓解。
练习:DVWA 的 SQL Injection(low / medium / high 三档),每一档先手注成功,再打开 WAF 看能不能拦。medium 档把输入框改成下拉菜单,很多人以为绕不过去了,其实用 Burp 改请求参数就穿了 —— 前端限制不是安全措施。
怎么防
- SQL:参数化查询,永远不拼字符串
- XSS:输出编码,按上下文(HTML / 属性 / JS / URL)分别处理
- 命令:不用
system拼参数,用参数数组 - 模板:不用
eval,不把用户输入当模板
A06 不安全设计
本质:代码一行 bug 都没有,但业务规则本身可以被滥用。
长什么样
这是唯一一类”修代码修不掉”的问题,因为问题在设计图里:
- 找回密码靠密保问题,而密保问题的答案在社交媒体上都能查到
- 优惠券可以叠用,因为设计时没人想过”同一个用户能不能领 100 张”
- 登录接口没有限速,密码再强也能被慢慢撞
- 转账流程是”扣款 → 到账 → 记账”,中间断了不回滚(这条其实和 A10 重叠)
挑项目里的一个功能,比如”分享文章链接”,填这张表:
- 正常流程是什么?用户点分享 → 生成链接 → 别人打开能看
- 如果我把链接里的文章 id 改成别的?→ 越权(A01)
- 如果我把链接发给一万个人?→ 有没有限速(A06)
- 如果链接永不过期?→ 分享出去的能不能收回
- 如果链接里带了我的身份?→ 别人拿到就变成我
- 如果生成链接的接口被脚本循环调用?→ 资源耗尽(A10)
这就是威胁建模的最小版本:不问”代码有没有 bug”,问”这个功能能被怎么用坏”。 官方推荐 STRIDE 模型:仿冒、篡改、否认、信息泄露、拒绝服务、权限提升,六项逐个过。
怎么防
- 设计阶段就写”滥用用例”,和正常用例一起评审
- 关键操作限速、限额、要二次确认
- 用成熟的方案,别自己发明认证/加密/支付流程
A07 认证失效
本质:“你是谁”这一步做错了。
长什么样
- 弱口令 + 没有登录失败锁定 = 可以无限撞
- 会话 ID 可预测(DVWA 的 Weak Session IDs 模块专门演示这个,它生成的 cookie 是
dvwaSession=1、2、3这样递增的) - 登出之后 session 在服务端还有效,抓到的 cookie 照样能用
- JWT 永不过期,或者签名算法可以被改成
none
练习:DVWA → Brute Force。low 档没有防护,直接用 Burp Intruder 跑字典;medium 档加了延迟,观察响应时间变化;high 档加了锁定。同一份字典,三档三种结果,这就是”认证强度”的差异。
怎么防
- 密码策略 + 登录限速 + 失败锁定
- 用成熟框架(OAuth / OIDC / 现成的库),别自己写认证
- 服务端必须能让会话失效;JWT 短有效期 + refresh token
- 强制 MFA
A08 软件或数据完整性失效
本质:东西被改过了,但你没验证。
长什么样
最典型的是反序列化。我写了个 30 行的 PHP:
<?phpclass Cache{ public $key; public $payload;
public function __destruct() { // 对象销毁时自动执行 —— 攻击者不需要"调用"任何函数 if ($this->key === 'gc') { system($this->payload); } }}
$data = $_GET['data'] ?? '';header('Content-Type: text/plain; charset=utf-8');
if ($data === '') { http_response_code(400); echo "usage: ?data=<base64>\n"; exit;}
// 开发者以为自己在"读一段缓存",实际是在执行攻击者指定的对象图$obj = @unserialize(base64_decode($data));
if (is_object($obj)) { echo "反序列化成功,得到对象: " . get_class($obj) . "\n";} else { echo "不是对象,结果: " . var_export($obj, true) . "\n";}echo "请求结束(对象的 __destruct 会在这之后执行)\n";先用一段正常人会传的数据(一个数组):
不是对象,结果: array ( 'name' => 'guest',)请求结束(对象的 __destruct 会在这之后执行)什么都没发生,很安全 —— 这就是让人放松警惕的地方。
现在构造一个对象。payload 原文:
O:5:"Cache":2:{s:3:"key";s:2:"gc";s:7:"payload";s:2:"id";}解释一下这段看起来像乱码的东西:
O:5:"Cache"—— 我要一个Cache类的对象(类名 5 个字符):2:—— 它有两个属性s:3:"key";s:2:"gc";—— 属性key的值是字符串gcs:7:"payload";s:2:"id";—— 属性payload的值是字符串id
base64 编码后塞进 URL:
curl "http://127.0.0.1:8000/vuln/a08-deserialize.php?data=Tzo1OiJDYWNoZSI6Mjp7czozOiJrZXkiO3M6MjoiZ2MiO3M6NzoicGF5bG9hZCI7czoyOiJpZCI7fQ%3D%3D"实测输出:
反序列化成功,得到对象: Cache请求结束(对象的 __destruct 会在这之后执行)uid=33(www-data) gid=33(www-data) groups=33(www-data)id 命令执行了,以 web 服务用户的身份。 整个过程我没调用任何函数,只是”传了一段数据”。执行发生在页面输出完之后,因为 __destruct 是对象销毁时自动触发的。
把 id 换成 cat /etc/passwd、换成反弹 shell,就是完整的 RCE。这就是 CWE-502 为什么常年排在危险榜前面。
练习:把上面两个 payload 都跑一遍,对比输出。然后把 system($this->payload) 改成 error_log(...) 再跑一次 —— 命令不再执行,但对象照样能构造。这证明漏洞在 unserialize,不在 system。
怎么防
- 别对不可信数据用原生反序列化(PHP
unserialize、JavaObjectInputStream、Pythonpickle) - 用 JSON / XML(配白名单)这种纯数据类型
- 必须用时,加签名校验(HMAC),并且限制可实例化的类
- 引入外部脚本用 SRI(
integrity属性),构建产物做签名
A09 日志与告警失效
本质:被打了没记录;或者记了,但没人被叫醒。
长什么样
这条我用自己的 WAF 实测,因为它是唯一一个”能通过日志观察到”的类别。
把 WAF 设成审计模式(只记录不拦截),然后打四条不同手法的载荷:
for p in "1' OR '1'='1" "1'; DROP TABLE users--" "../../etc/passwd" "<script>alert(1)</script>"; do curl -s -o /dev/null "http://127.0.0.1:8889/vulnerabilities/sqli/?id=${p}&Submit=Submit"done四条全部返回 302(因为没登录 cookie),看起来一切正常。然后看审计日志:
11:38:36 ?id=1' OR '1'='1 score=100 action=log would_block=True └─ 1021 SQL 注入 · 恒真条件(55); 1024 SQL 注入 · 引号闭合特征(45)11:38:36 ?id=1'; DROP TABLE score=185 action=log would_block=True └─ 1024 引号闭合特征(45); 1026 破坏性语句(70); 1027 堆叠查询(70)11:38:36 ?id=../../etc/passwd score=170 action=log would_block=True └─ 1001 路径穿越(70); 1010 敏感文件访问(100)11:38:36 ?id=<script>alert(1) score=115 action=log would_block=True └─ 2001 XSS · script 标签(70); 2007 弹窗与危险函数(45)四条全部 would_block=True,意思是”如果开了拦截,这四条都该被挡”。但 action=log,所以它们全被放过去了。
这就是 A09 的核心场景:日志完美记录了每一次攻击,分数、规则、载荷、时间戳一样不缺。但如果没有人配告警、没有人看这个文件,那它就只是一堆躺在磁盘上的 JSON。
日志本身不会保护你。从”有日志”到”有告警”中间隔着的那个东西,才是这条真正要讲的。
练习:就是上面这套。跑完之后问自己三个问题:
- 我的项目里,这份日志谁在看?
- 什么条件下会有人被通知?还是说”出事了自己去翻”?
- 如果同一个 IP 一分钟打 100 次,我能在第几次知道?
我给自己的答案是:第一个”只有我偶尔翻”,第二个”没有条件”,第三个”永远不知道”。三个答案都是错的,这就是我的 A09 现状。
怎么防
- 记”安全事件”而不只是 access log:登录失败、权限拒绝、输入被拦、异常状态码
- 日志集中收集,不要只留在单机
- 配阈值告警:单 IP 失败次数、WAF 拦截率突增、5xx 比例
- 日志里别记密码和完整 Cookie(这是 A09 和 A04 的交界)
A10 异常处理不当
本质:出错的时候,程序反而松手了。
官方管这个叫 fail open(失败即放行),反义词是 fail closed(失败即拒绝)。安全场景里几乎永远该选后者。
长什么样
我写了个 30 行的鉴权函数,逻辑很常见:
<?phpfunction is_admin(array $user): bool{ try { // $user['role'] 可能根本不存在,PHP 8 先报 Warning,然后当 null 比较 if ($user['role'] === 'admin') { return true; } throw new RuntimeException('role check failed'); } catch (RuntimeException $e) { error_log('[auth] ' . $e->getMessage()); return true; // ← 出事就松手,等于没有鉴权 }}
$user = ['name' => $_GET['name'] ?? 'guest'];if (isset($_GET['role'])) { $user['role'] = $_GET['role'];}catch 里那句 return true 的开发心理很好理解:“先别让用户卡住,出错也放过去,回头查日志”。上线之后它就是一道没有锁的门。
传一个明显不是管理员的角色:
curl "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest&role=user"200 OK —— 已进入管理面板name: guestrole: user
[HTTP 200]role=user,进了管理面板。
更糟的是不传 role:
curl "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest"Warning: Undefined array key "role" in .../a10-failopen.php on line 11200 OK —— 已进入管理面板name: guestrole: (未提供)
[HTTP 200]参数缺了反而直接进后台。 因为缺参数触发了异常路径,而异常路径是放行的。同时那个 Warning 还把文件绝对路径和行号吐了出来 —— A10 和 A02 在这里合流了。
修法只有一个字:把 return true 改成 return false(或者直接 throw)。
} catch (RuntimeException $e) { error_log('[auth] ' . $e->getMessage()); return false; // fail closed:不确定就拒绝}练习:就是上面三条 curl。然后做一次单变量对照:
- 把
return true改成return false,重跑 → 应该 403 - 把
display_errors关掉(ini_set('display_errors','0')),重跑缺参数那条 → 403 且不再泄露路径 - 把两条改动都还原 → 又回到 200
三次请求,两个开关,一一对应。 这就是”实测证据”和”我觉得应该”的区别。
怎么防
- 默认拒绝:异常、超时、未知状态,一律当作不通过
- 全局兜底异常处理器,别让任何一个异常漏到用户面前
- 统一错误页,不显示堆栈、SQL、路径
- 事务中断要整体回滚(fail closed 的另一种形态)
- 加限速和资源配额 —— 很多异常是”没有上限”造成的
参考
- OWASP Top 10:2025 官方页面:https://owasp.org/Top10/2025/en/
- 引言(版本变化说明):https://top10.owasp.org/2025/0x00_2025-Introduction/
- OWASP WSTG(怎么测):https://owasp.org/www-project-web-security-testing-guide/
- OWASP ASVS(怎么验收):https://owasp.org/www-project-application-security-verification-standard/
- OWASP Cheat Sheet 系列(怎么修):https://cheatsheetseries.owasp.org/
- CWE 列表:https://cwe.mitre.org/
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!