搭个靶场熟悉OWASPTop10,感觉好多岗位要求要熟悉这个

6118 字
31 分钟
搭个靶场熟悉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() failed
target : /var/log/app/access-today.log
cwd : /var/www/html/vuln
php : 8.5.11
server : 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,先访问:

Terminal window
curl -i http://127.0.0.1:8000/backup/
HTTP/1.1 403 Forbidden
Date: Thu, 22 Oct 2026 03:42:22 GMT
Server: Apache/2.4.68 (Debian)
Content-Length: 316
Content-Type: text/html; charset=iso-8859-1

403 说明 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 继续往外推毒版本)。

长什么样

我对自己的博客项目跑了一遍依赖审计:

Terminal window
cd LYH-Blogs
pnpm audit

结果(实测):

122 vulnerabilities found
Severity: 12 low | 45 moderate | 63 high | 2 critical

两条 critical 的详情:

critical Astro: Remote code execution through AVIF image optimization
Package astro
Vulnerable versions <7.2.8
Patched versions >=7.2.8
Paths . > @astrojs/mdx@5.0.6 > astro@6.3.5
. > astro@6.3.5
critical node-tar: Decompression/parse DoS via unlimited input
Package tar
Vulnerable versions <=7.5.18
Patched versions >=7.5.19
Paths . > 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 拿到的那份备份文件。两个看起来很像密码的东西:

5f4dcc3b5aa765d61d8327deb882cf99
e10adc3949ba59abbe56e057f20f883e

MD5 值。既然是 MD5,就直接算一下常见口令(实测,用 macOS 自带的 md5 命令):

Terminal window
for w in password 123456 admin; do
printf '%-10s -> %s\n' "$w" "$(printf '%s' "$w" | md5)"
done
password -> 5f4dcc3b5aa765d61d8327deb882cf99
123456 -> e10adc3949ba59abbe56e057f20f883e
admin -> 21232f297a57a5a743894a0e4a801fc3

两个哈希全部命中。admin 的密码是 password,gordon 的密码是 123456。

这里的关键不是”MD5 不安全”,而是”无盐的快速哈希根本不能用来存密码”。 MD5 设计目标是快,一秒能算几百万次,正好方便暴力破解。即使换成 SHA-256,问题一样 —— 快就是问题。

正确的做法是专门为存密码设计的慢哈希:

// 错的
$hash = md5($password);
// 对的
$hash = password_hash($password, PASSWORD_DEFAULT); // bcrypt
var_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。打一条最经典的恒真条件:

Terminal window
# 直接打靶场(无 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 302

302 是因为没带登录 cookie,被弹回登录页了。但注意:应用已经处理了这个请求,SQL 也拼进去执行了,只是结果没返回给我。响应码干净不代表没被打到。

现在把同一个请求指向我自己写的 WAF(block 模式):

Terminal window
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 403

WAF 的审计日志里记着这次判决:

score : 100 / 阈值 100
action : block resp_status: 403
backend_status: None (None = 请求没有走到后端)
命中 : 1021 SQL 注入 · 恒真条件 +55; 1024 SQL 注入 · 引号闭合特征 +45

backend_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:

<?php
class 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 的值是字符串 gc
  • s:7:"payload";s:2:"id"; —— 属性 payload 的值是字符串 id

base64 编码后塞进 URL:

Terminal window
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、Java ObjectInputStream、Python pickle)
  • 用 JSON / XML(配白名单)这种纯数据类型
  • 必须用时,加签名校验(HMAC),并且限制可实例化的类
  • 引入外部脚本用 SRI(integrity 属性),构建产物做签名

A09 日志与告警失效#

本质:被打了没记录;或者记了,但没人被叫醒。

长什么样

这条我用自己的 WAF 实测,因为它是唯一一个”能通过日志观察到”的类别。

把 WAF 设成审计模式(只记录不拦截),然后打四条不同手法的载荷:

Terminal window
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。

日志本身不会保护你。从”有日志”到”有告警”中间隔着的那个东西,才是这条真正要讲的。

练习:就是上面这套。跑完之后问自己三个问题:

  1. 我的项目里,这份日志谁在看?
  2. 什么条件下会有人被通知?还是说”出事了自己去翻”?
  3. 如果同一个 IP 一分钟打 100 次,我能在第几次知道?

我给自己的答案是:第一个”只有我偶尔翻”,第二个”没有条件”,第三个”永远不知道”。三个答案都是错的,这就是我的 A09 现状。

怎么防

  • 记”安全事件”而不只是 access log:登录失败、权限拒绝、输入被拦、异常状态码
  • 日志集中收集,不要只留在单机
  • 配阈值告警:单 IP 失败次数、WAF 拦截率突增、5xx 比例
  • 日志里别记密码和完整 Cookie(这是 A09 和 A04 的交界)

A10 异常处理不当#

本质:出错的时候,程序反而松手了。

官方管这个叫 fail open(失败即放行),反义词是 fail closed(失败即拒绝)。安全场景里几乎永远该选后者。

长什么样

我写了个 30 行的鉴权函数,逻辑很常见:

<?php
function 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 的开发心理很好理解:“先别让用户卡住,出错也放过去,回头查日志”。上线之后它就是一道没有锁的门。

传一个明显不是管理员的角色:

Terminal window
curl "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest&role=user"
200 OK —— 已进入管理面板
name: guest
role: user
[HTTP 200]

role=user,进了管理面板。

更糟的是不传 role:

Terminal window
curl "http://127.0.0.1:8000/vuln/a10-failopen.php?name=guest"
Warning: Undefined array key "role" in .../a10-failopen.php on line 11
200 OK —— 已进入管理面板
name: guest
role: (未提供)
[HTTP 200]

参数缺了反而直接进后台。 因为缺参数触发了异常路径,而异常路径是放行的。同时那个 Warning 还把文件绝对路径和行号吐了出来 —— A10 和 A02 在这里合流了。

修法只有一个字:把 return true 改成 return false(或者直接 throw)。

} catch (RuntimeException $e) {
error_log('[auth] ' . $e->getMessage());
return false; // fail closed:不确定就拒绝
}

练习:就是上面三条 curl。然后做一次单变量对照:

  1. 把 return true 改成 return false,重跑 → 应该 403
  2. 把 display_errors 关掉(ini_set('display_errors','0')),重跑缺参数那条 → 403 且不再泄露路径
  3. 把两条改动都还原 → 又回到 200

三次请求,两个开关,一一对应。 这就是”实测证据”和”我觉得应该”的区别。

怎么防

  • 默认拒绝:异常、超时、未知状态,一律当作不通过
  • 全局兜底异常处理器,别让任何一个异常漏到用户面前
  • 统一错误页,不显示堆栈、SQL、路径
  • 事务中断要整体回滚(fail closed 的另一种形态)
  • 加限速和资源配额 —— 很多异常是”没有上限”造成的

参考#

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

相关文章 智能推荐
1
一个 500 错误页泄露了源码、绝对路径和短信网关口令
漏洞 扫描学校漏洞的时候发现一次 ASP.NET 详细编译错误(YSOD)信息泄露复盘:customErrors 未关闭、生产站点带着编译错误上线、凭据硬编码在代码里,三件事叠在一起,把业务源码和第三方短信账号一起交给了匿名访问者。
2
从一条报错到全校会话库:某高校 SSO + GraphQL 数据网关的完整攻击链
漏洞 一个未授权入口如何被逐级放大成"25 万师生 PII + 35 万登录态 + 任意 SQL 执行"。我挖出来的第一个高危
3
记录练习OWASP
漏洞 练习一下就可以完结这个环节了
4
CVE-2026-64638(WordPress XSS2Shell)我的第一个网站就是因为这个给挂马的我要看看是怎么回事
漏洞 我倒要看看我之前几个月的心血是怎么没的~~独立复现了 WordPress CVE-2026-64638 漏洞,构建 Docker 三靶机(漏洞/修复/加固版),用纯 stdlib Python 手搓 HTTP 报文验证 XSS 差异,通过 CDP 精准触发跨窗口点击实现 RCE。所有结论均经单变量对照实验与原始日志验证,最终输出含完整踩坑记录与防御建议。
5
CVE--2021-41773(Apache HTTP Server 2.4.49 路径穿越)漏洞复现
漏洞 作为我的第一个漏洞复现,Apache 2.4.49 对请求路径做 URL 解码后,没有再检查越界,于是 /icons/.%2e/../ 这类路径能穿出网站根目录。默认配置返回 403 打不通;授权一旦放宽就是任意文件读取,再开启 CGI 还能指向 /bin/sh 执行命令。
随机文章 随机推荐
Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
52
分类
20
标签
32
总字数
311,598
运行时长
0 天
最后活动
0 天前

文章目录