授权与合规声明
本文全部操作对象均为自建隔离靶场(本机容器或隔离虚拟机),涉及安全测试的环节必须以取得合法授权为前提。未经授权的渗透测试违反《中华人民共和国网络安全法》与《刑法》相关条款,须承担相应法律责任。本文只讲环境配置、版本对照与靶场隔离,不含任何攻击步骤、利用载荷与绕过手法,请勿将文中环境指向任何非自有系统。
一、先看现象:同一条正则,短输入秒回、长输入不动
1.1 转圈的那几十秒,日志里什么都没留下
一个再普通不过的字段校验。用户提交一段「备注」,后端先过一条正则,确认文本的构成符合约定。这条正则写在validate()里,一行,注释只有四个字:只允许字母。上线前你自己试过:空串、一个字母、带数字的、带中文的,全部一次通过,耗时看不出差别。
然后某个工作日的下午,接口开始转圈。
不是所有请求都慢,是偶发的一两个。用户并没有做什么特别的操作,他只是把一份文档里的一大段说明文字粘进了备注框——不是为了攻击你,是真的想提交这段备注。请求发出去,前端一直等;网关在预定的时间点把连接掐断;应用日志里有「请求开始」,没有「请求结束」;机器上那条 CPU 曲线,在那几十秒里有一个很尖的峰,随后落回去。
重启之后,一切恢复正常,你拿同样的账号、同样的浏览器再提交一次,怎么都复现不出来。
1.2 把「感觉慢」换成一张表
复现不出来的原因,是触发条件不在「请求」这一层,而在「输入」这一层:它要同时满足两个条件——足够长,并且以一个让匹配失败的方式结束。你手工重试时粘的是能通过的那段,走的全是快路径,自然看不出问题。
要判断这类问题,不用读引擎源码,只要把长度当成唯一变量。固定模式、固定机器、固定输入的字符构成,只改长度,每一档跑多轮,取中位数。下面这张表是 2026-10-25 在本机跑出来的结果,模式是一条用于校验「整段只由同一种字符组成」的表达式,输入是本地合成的字符串,末尾多加一个不属于该类字符的符号,使它必然匹配失败。
| 连续字符个数 n | 平均耗时(毫秒) | 相对上一档 |
|---|---|---|
| 14 | 0.38 | — |
| 16 | 1.49 | ×3.9 |
| 18 | 5.83 | ×3.9 |
| 20 | 21.80 | ×3.7 |
| 22 | 82.94 | ×3.8 |
上表为本机实测(CPython 3.13 + macOS),输入全部是本地合成的字符串,不指向任何在线服务;数值随机器变化,倍数关系不变。
读这张表,别读毫秒,读最后一列。四档的比值都落在 3.7 到 3.9 之间,非常稳定:每加 2 个字符,耗时乘 4。也就是说,每加 1 个字符,耗时大约乘 2。
下面这段是把测法固化成脚本的骨架,模式和输入都从脚本外部给。表里那五个数值就是照这套逻辑跑出来的;脚本本身没有再逐字复跑,所以按规矩标了待验证。
⚠️代码待验证
importos,re,time,statistics PATTERN=re.compile(os.environ["RE_PATTERN"])# 待测的那一条,从本地配置读INPUTS=[l.rstrip("\n")forlinopen(os.environ["RE_INPUTS"])]defmeasure(text,rounds=9):samples=[]for_inrange(rounds):t0=time.perf_counter()PATTERN.search(text)samples.append((time.perf_counter()-t0)*1000)returnstatistics.median(samples)fortextinINPUTS:print(f"len={len(text):>3}median={measure(text):9.3f}ms")1.3 该记住的不是毫秒,是倍数
毫秒数最不可迁移。同一段代码在同事的笔记本上可能快五倍,在容器里可能慢三倍——CPU 型号、主频、是否有别的进程在抢,全都在动。拿本文的 82.94 毫秒去和别人对账,没有意义。
有意义的是那个比值。它描述的是「耗时会怎么随输入长度变化」,这是一个形态,不是一次测量。形态由写法决定,不由机器决定。所以你在自己机器上测一遍,只要看到「每加两个字符乘 4」这个节奏,就已经能判定这条写法有问题了,不需要任何人的数值来背书。
也正因为它是形态,它才值得警惕:从 n=14 到 n=22,只多了 8 个字符,耗时从 0.38 毫秒变成 82.94 毫秒,差了约 218 倍。这不是「慢了一点」,是两种不同的东西。
本章可以带走的一句:一条正则慢不慢,不看它在短输入上多快,看它的耗时怎么随长度变。
二、回溯是什么:匹配失败之后,引擎还得回头
2.1 匹配不是看一眼,是一路做选择
正则引擎读你的模式,从左到右一个字符一个字符地对。凡是遇到量词,它就要做一个决定:这一个「重复」,我到底重复几次?
把它想成走迷宫。你站在入口,前面每个岔口都要选一条路。选对了,继续往前;选错了,走到底发现是死胡同,就得退回到最近的那个岔口,换另一条路再走一次。这个「退回去、换条路」的动作,就是回溯。
关键在于:引擎在走的时候,并不知道哪条路是对的,它只能靠试。所以「重复几次」这个决定不是一次定下来的,而是边走边改。
⚠️代码待验证
模式允许:把一段字符切成若干段,每段至少一个 切法一 → 走到底 → 不通 → 退回 切法二 → 走到底 → 不通 → 退回 切法三 → …… 一次「退回」= 一次回溯 走法有多少种,退回就可能有多少次2.2 失败的时候,引擎要把做过的选择全部重走一遍
这就解释了一件很多人没想通的事:为什么最慢的情况,恰恰是「这条输入不该被通过」的时候。
如果输入能匹配,引擎走到某个位置发现「成了」,就可以收工。但如果输入注定不匹配,引擎必须把所有可能的路都走完,才敢下结论说「不匹配」。回溯型引擎在这里的代价,是它要把已经做过的选择重新走一遍;而这些失败路径的数量,可以随输入长度指数增长。
Python 的re、Perl、PCRE、Java 的java.util.regex都属于这一类。这句话的分量在于:这是回溯的本质代价,不是 bug,升级依赖、换小版本、加编译缓存都不会改变它。
2.3 指数是从「有多少种走法」里长出来的
指数不是玄学,它可以数出来。假设输入是 n 个一模一样的字符,而你的模式允许「把这一段切成若干段、每段至少一个」,那么切法有多少种?
| 连续字符个数 n | 所有可能的分段方式数量 | 相对上一档 |
|---|---|---|
| 4 | 8 | — |
| 10 | 512 | 64 |
| 14 | 8192 | 16 |
| 18 | 131072 | 16 |
| 22 | 2097152 | 16 |
每一档加 4 个字符,数量乘 16——也就是每加 1 个字符乘 2,每加 2 个字符乘 4。这和第一章那张计时表最后一列的 3.7 到 3.9,是同一件事的两种说法:一边是「有多少种走法」,一边是「要花多少时间」。
引擎不会把每一种走法都完整走完,它剪掉一部分明显重复的分支,所以真实耗时不会精确等于上表的行数;但量级关系对得上。
本章可以带走的一句:慢的不是匹配,是匹配失败之后那趟回头路。
三、三种放大器:什么样的写法会把回头路变多
3.1 嵌套量词:同一段字符被反复重新切分
第一种形态,是量词套着量词。
具体说:一个分组本身用「一个或多个」修饰,这个分组整体外面,又套了一层「一个或多个」。写成抽象的形态就是这样:
⚠️代码待验证
( X+ )+这里必须说清楚:上面这行只是形态示意,不作为可直接使用的模式。它想表达的只有一件事——外层每选一种分法,内层就得把同一段字符重新切一遍,同一批字符被数了很多次。你自己代码里真正长什么样,要看你的分组和字符集,本文不给任何可以直接拿去用的完整写法。
识别方法很简单:在正则里找「右括号后紧跟量词、而括号内部本身也以量词结尾」的位置。看到这种结构,先假设它有问题,再去测。
3.2 相邻的重叠分支:两条路通向同一个地方
第二种形态,是分支之间能匹配同一段字符。
(A|B)这样的选择结构里,如果 A 和 B 能吃掉同一批字符——比如一个是另一个的前缀,或者两者的字符范围有交集——那么对同一段输入,引擎就有两条路可走。匹配失败的时候,两条路都得试;如果这个分支后面还跟着量词,那么每一条路的每种长度组合,都要各试一遍。
这种形态更隐蔽,因为它看起来「很正常」:谁写校验规则没写过几个备选分支呢?问题不在有分支,而在分支之间有重叠,且分支后面还有量词在放大它。
判断方法:把每个分支能接受的字符集合写出来,看有没有交集。有交集,且分支之后还跟着量词,就归到这一类。
3.3 无锚点全扫:每个起点都当一次开头
第三种形态,跟模式本身无关,跟你怎么调用有关。
如果一条用于「整段校验」的正则,没有写起点锚定,而调用时用的又是「在字符串里找一处匹配」的接口,那么引擎会把每一个字符位置都当成一次新的开头,各试一遍。输入长 n,就等于把前面的代价乘上 n。
这一条的放大倍数最温和——只是线性的一倍——但它最容易和前面两种叠在一起。一个没有锚定的、带嵌套量词的校验,代价是「每个起点 × 每条回头路」,两者相乘。
| 形态 | 在代码里怎么认(描述,不给可用写法) | 放大的是什么 | 一句话识别 |
|---|---|---|---|
| 嵌套量词 | 分组内部以量词结尾,右括号外又跟一个量词 | 同一段字符的切分次数 | 重复的重复 |
| 重叠分支 | 两个分支的字符集有交集,分支后还跟着量词 | 每个位置的可选路径数 | 两条路走同一段 |
| 无锚点全扫 | 做整段校验,却没有起点锚定,且用「找一处」的接口 | 起点数量 | 每个位置都当开头 |
本章可以带走的一句:三种形态单独看都只是「倍数」,叠在一起才是指数。
四、为什么换个语言就没事:两条不同的技术路线
4.1 走回头路,和不走回头路
正则的实现有两条路线。
一条是回溯型:引擎维护一个「选择栈」,走不通就弹栈重试。Perl、PCRE、Python 的re、Java 的java.util.regex都是这一路。它的优点是表达能力强、贴着人的直觉走;代价就是上一章讲的回头路。
另一条是自动机型:把模式编译成一个状态机,一次扫过去,不保留「回头重选」的机制。RE2 就属于这一路。RE2 官方对自己的定位,是把自己放在「backtracking regular expression engines like those used in PCRE, Perl, and Python」的对立面——它明确承认,自己说的是另一条路线,不是同一件事的更快版本。
4.2 Go 那句线性承诺,值得逐字读
Go 的regexp包文档里有一句很硬的话:
The regexp implementation provided by this package is guaranteed to run in time linear in the size of the input.
逐字抠一下:guaranteed——是保证,不是「通常」;linear——线性;in the size of the input——相对于输入规模。合起来的意思是:输入翻倍,耗时最多翻个常数倍,不会出现第一章那种「每加两个字符乘 4」。
注意它没承诺什么。它没承诺更快,也没承诺所有模式都能写。它承诺的是可预测。这个区别很重要:如果你正在排查的是一个偶发的、无法复现的超时,你要的恰恰是可预测,而不是平均快 30%。
⚠️代码待验证
python3-c"import re,sys; print(sys.version); print(re.__file__)"# 看 Python 这一侧go version# 看 Go 这一侧go doc regexp|head-n20# 线性承诺那句就在这份文档里| 实现 | 路线 | 复杂度形态 | 本文依据 |
|---|---|---|---|
Pythonre | 回溯型 | 失败路径可随长度指数增长 | RE-03 |
| Perl / PCRE | 回溯型 | 同上 | RE-03、RE-05 |
Javajava.util.regex | 回溯型 | 同上 | RE-03 |
Goregexp | 自动机型(RE2 系) | 线性承诺 | RE-04、RE-05 |
| JavaScript 引擎 | 本文不展开 | 各引擎实现与版本差异需自行核对 | RE-11(待验证) |
4.3 换语言看起来解决了,其实只是换了个位置
有人会想:那我用 Go 写这个服务不就行了。可以,但你换的不是一行代码,是整条技术栈。更常见的情况是:同一个校验规则,Python 的老服务里有一份,Go 的新网关里有一份,两边行为不一样;出问题的时候,你在 Go 那边怎么都复现不出来,因为问题在 Python 那边。所以「换引擎」是架构决策,不是写法技巧。
本章可以带走的一句:线性引擎买到的是可预测,不是更快;换引擎是改架构,不是改写法。
五、怎么在自己机器上判定:一张长度与耗时的表
5.1 测法:只改长度,别的都不动
这套测法只有四条纪律。
固定模式,一次只测一条,别把好几条拼在一个脚本里跑。固定输入的字符构成——第三章讲过,形态是由「字符怎么重复」决定的,所以除长度外每档输入应当完全同构。每档跑多轮取中位数,时钟精度、垃圾回收、调度抖动都会给你离群值。最后,不要在共享机器上测,更不要在线上服务上测:你要的是这条正则的形态,不是这台机器此刻的负载。
把第一章那段脚本的输入长度依次取 10、20、40、80,别的都不动,你会拿到四行数字。接下来是判读。
5.2 判读:看第二次翻倍的倍数有没有继续变大
| 长度翻倍后的耗时倍数 | 大致形态 | 要不要管 |
|---|---|---|
| 约 ×2 | 线性 | 通常不用管 |
| 约 ×4,且再翻一倍仍是 ×4 | 平方 | 要管,长输入会明显拖慢 |
| 倍数本身还在随长度变大 | 指数形态 | 必须先限长,再改写 |
| 忽高忽低、没有规律 | 测法有问题,或被别的负载干扰 | 先修测法,别下结论 |
平方和指数的区别,就在第二列和第三列之间:平方的倍数是个常数(翻一倍总是 ×4),指数的倍数是「越翻越大」。区分它们的办法很简单——多做一次翻倍。只翻一次,两者看起来一模一样;翻两次,答案就出来了。
还有一个容易误判的点:如果四行数字都很快,也别急着下结论。可能你选的输入没有让匹配失败,全程走的是快路径。校验类正则的问题,几乎都藏在「不通过」的那一侧。
5.3 三个会让你白忙一场的坑
一是输入没构造对。同一段文本,结尾多一个字符和少一个字符,走的可能是完全不同的路径。二是只测一次。单次测量里混着进程启动、模块导入、缓存预热,你测到的是环境,不是正则。三是用了带结果缓存的框架去测,第二次调用直接命中缓存,你会得到一条漂亮但虚假的直线。这三条避开,你得到的形态结论才有意义。
本章可以带走的一句:你不需要读引擎源码,只需要一张长度与耗时的表。
完整版对照资料:本文把「长度与耗时」的测法、判读表、三种形态的识别要点整理成了一页速查,和本章这套判定流程是同一份东西的浓缩版。放在资料包里,扫码即可获取:
六、改法:每一条都有代价,没有免费的
6.1 最便宜的两步:限长,然后加锚点
第一步是限长。这件事必须在正则之前做:先判断长度,超了直接拒,根本不让它进正则。如果顺序反了,你连「这段输入有多长」这件事都要花在匹配里,限长就白限了。
第二步是加锚点。做整段校验的正则,就该用「整段匹配」的语义,而不是「在字符串里找一处」。锚点让起点唯一,第三章里那个「每个位置都当一次开头」的放大器就直接没了。
两件事都有代价。限长要你定一个业务上限——备注到底是 500 字还是 5000 字,这是产品决策,技术侧替不了你。加锚点会改变语义:原来「文本里包含一段符合规则的内容」就通过,现在变成「整段都符合才通过」,上线前必须把正例、反例、边界值全部重跑一遍。
下面是一个把顺序摆对的写法,长度检查在前,正则在后:
⚠️代码待验证
MAX_LEN=500defvalidate_note(text:str)->bool:iftextisNone:returnFalseiflen(text)>MAX_LEN:# 先卡长度,最便宜的一道returnFalseifnottext.isascii():# 再做字符集层面的粗筛returnFalsereturnPATTERN.fullmatch(text)isnotNone# 最后才是结构性校验6.2 拆平嵌套,改掉重叠分支
嵌套量词的改法,是把「重复的重复」拍平成「一次重复 + 明确的字符集」。原来靠分组表达的结构,改成用字符类和定长结构表达,让同一段字符只有一种切分方式。
重叠分支的改法有两种:一是把分支改成互斥的(调整字符范围,让两者不可能同时匹配同一段),二是调整分支顺序并配合锚定,让引擎尽早排除掉不可能的路。前者更彻底,后者改动更小。
代价是等价性。任何改写都会引入「原来能过、现在过不了」或者反过来的风险。所以改写之后必须做一件事:把这条规则曾经拒绝过的真实样本(脱敏后的)、曾经通过的真实样本,各挑一批重跑。没有这批样本,你不知道自己改的是性能还是语义。
6.3 换引擎、上原子组、加超时,各自代价不同
占有量词和原子组,是 PCRE 系提供的能力:它告诉引擎「这一段一旦匹配上,就别再回头改主意了」。这直接把回头路砍掉,但语义也变了——原本靠回溯才能匹配到的输入,改写后可能匹配不上;而且不是每种语言都有这个语法,跨语言迁移时要注意。
换线性引擎是最彻底的一条,代价也最大:模式语法是另一个集合,要逐条核对;而且第四章说过,你换的是技术栈,不是一行代码。
加超时最容易做,也最容易骗自己。它只是止损:超时之后请求会失败,业务侧必须能处理这种失败,否则你只是把「一直转圈」换成了「转一会儿就失败」,它不是修复。
另外一件事必须标清楚:各语言引擎回溯上限的默认值,属于本文的待验证项(RE-11),本文不引用任何具体数值。要在自己的环境里确认,只能按你所用的那个实现的文档核对,别照抄别人的数字。
| 手段 | 改什么 | 代价 | 什么时候不该用 |
|---|---|---|---|
| 限长 | 在正则之前卡长度 | 要定业务上限 | 输入本身必须完整保留时 |
| 加锚点 | 把「找一处」改成「整段」 | 语义变化,要重跑用例 | 业务确实是「包含即通过」 |
| 拆平嵌套 | 改变分组与量词结构 | 等价性风险 | 缺真实样本可回归时 |
| 原子组 / 占有量词 | 禁止回溯 | 语义变化,且非所有语言支持 | 需要跨语言复用同一条模式 |
| 换线性引擎 | 换实现 | 语法子集不同,迁移成本高 | 只是给老服务打补丁时 |
| 加超时 | 加一层时限 | 只止损,不修复 | 把它当成最终方案时 |
本章可以带走的一句:所有改法都是拿语义或架构上的让步,换一份可预测的耗时。
七、把它放进校验流程的哪一步
7.1 四道闸的顺序不能乱
一条输入从进来到被使用,按理要过四道闸,顺序是有讲究的:
- 长度与体积上限——用最便宜的方式挡掉大部分异常输入;
- 编码与字符集粗筛——只允许该字段该有的字符类别;
- 结构与格式校验——也就是正则该待的地方;
- 业务规则——唯一性、频率、状态合法性。
第一章那个场景的问题,本质上是把第 3 道闸放在了第 1 道前面:正则成了第一个接触原始输入的东西,于是一段很长的、注定不通过的文本,把全部算力都花在了「证明它不通过」上。
还有一个常被忽略的点:入口不止表单。JSON 请求体里的字段、上传文件的文件名、请求头里的自定义字段、日志解析、批量导入的每一行、消息队列里的消息体——每一个都是原始输入的入口。你只在表单那一处加了限长,别的入口照样能把长输入送进来。
7.2 给每条正则留一份可审的记录
正则写在一行里,看不出成本,所以它需要一份额外记录,至少六栏:模式、用途、允许的输入上限、有没有锚定、有没有嵌套量词或重叠分支、最近一次复测的结论与日期。这份记录的价值不在当下,而在半年后有人要改这条规则的时候——他能看见「这条测过,当时是平方形态,靠限长兜住的」,而不是只看到一行注释,然后照着感觉改。
7.3 边界:这套方法管什么、不管什么
它管的是写法层面的耗时形态:怎么测、怎么读表、怎么改、改在哪一步,全部在你自己的机器和你自己的代码上完成。它不管「怎么去验证某个具体服务扛不扛得住」这类问题:本文所有计时都在本机用合成字符串完成,不给针对具体服务的验证方法,也不给可以直接拿去用的完整模式与配套输入。这类验证只要换一个目标,性质就完全不同了。
本章可以带走的一句:正则不该是第一个接触原始输入的东西。
完整版对照资料:本文第七章这套「四道闸 + 一条正则一份记录」的落地清单,已经整理成可直接照着填的表格,和本章的顺序设计一一对应。放在资料包里,扫码即可获取:
附表 A:本文引用事实与官方出处对照表
| 序号 | 事实 | 出处 | 核验日期 | 本文位置 |
|---|---|---|---|---|
| 1 | 计时环境为 CPython 3.13 + macOS,本机实测 | RE-01 | 2026-10-25 | 第一章 1.2 |
| 2 | n=14/16/18/20/22 对应 0.38 / 1.49 / 5.83 / 21.80 / 82.94 毫秒 | RE-01 | 2026-10-25 | 第一章 1.2 |
| 3 | 相邻两档的相对倍数落在 3.7 至 3.9 之间 | RE-01 | 2026-10-25 | 第一章 1.2 |
| 4 | 每加 2 个字符约乘 4,即每加 1 个字符约乘 2 | RE-01 | 2026-10-25 | 第一章 1.2、1.3 |
| 5 | 计时输入为本地合成字符串,不指向任何在线服务 | RE-01 | 2026-10-25 | 第一章 1.2 |
| 6 | 计时反映复杂度形态而非绝对速度,换机器数值会变、倍数关系不变 | RE-02 | 2026-10-25 | 第一章 1.3 |
| 7 | 回溯型引擎包括 Perl、PCRE、Pythonre、Javajava.util.regex | RE-03 | 2026-10-25 | 第二章 2.2、第四章 4.1 |
| 8 | 匹配失败时引擎需把已做过的选择重新走一遍 | RE-03 | 2026-10-25 | 第二章 2.2 |
| 9 | 失败路径数量可随输入长度指数增长 | RE-03 | 2026-10-25 | 第二章 2.2 |
| 10 | 上述代价是回溯的本质,不是 bug | RE-03 | 2026-10-25 | 第二章 2.2 |
| 11 | 放大器之一:量词套量词,内层重复被外层再重复一遍 | RE-06 | 2026-10-25 | 第三章 3.1 |
| 12 | 放大器之二:两个分支能匹配同一段字符,失败时任一条路都要试 | RE-06 | 2026-10-25 | 第三章 3.2 |
| 13 | 放大器之三:没有起点锚定,每个位置都要试一遍 | RE-06 | 2026-10-25 | 第三章 3.3 |
| 14 | Goregexp文档承诺运行时间相对输入规模线性 | RE-04 | 2026-10-25 | 第四章 4.2 |
| 15 | 该线性来自自动机路线,不做回溯 | RE-04 | 2026-10-25 | 第四章 4.2 |
| 16 | RE2 把自己定位为与 PCRE、Perl、Python 那类回溯引擎相对的另一条路线 | RE-05 | 2026-10-25 | 第四章 4.1 |
| 17 | 判定阈值:输入长度翻倍时耗时是否接近翻 4 倍或更多 | RE-07 | 2026-10-25 | 第五章 5.2 |
| 18 | 缓解手段:加锚点、拆平嵌套、限长、把校验挪到长度检查之后 | RE-08 | 2026-10-25 | 第六章 6.1、6.2 |
| 19 | 缓解手段:占有量词 / 原子组(PCRE 系) | RE-08 | 2026-10-25 | 第六章 6.3 |
| 20 | 缓解手段:换线性引擎(RE2 系实现) | RE-08 | 2026-10-25 | 第六章 6.3 |
| 21 | 各语言引擎回溯上限的默认值:待验证,本文不引用具体数值 | RE-11 | 2026-10-25 | 第六章 6.3 |
附表 B:术语速查表
| 术语 | 一句话解释 |
|---|---|
| 回溯 | 引擎走不通时退回上一个选择点、换一条路重试的动作 |
| 回溯型引擎 | 靠维护选择栈来匹配的实现,失败时要走完所有失败路径 |
| 自动机引擎 | 把模式编译成状态机、一次扫过、不回头重选的实现 |
| 嵌套量词 | 分组内部以量词结尾、分组外又套一个量词的结构 |
| 重叠分支 | 两个分支能匹配同一段字符,因而对同一输入存在多条路径 |
| 锚点 | 声明匹配只能从字符串开头或结尾开始的符号 |
| 原子组 / 占有量词 | 告诉引擎这一段匹配后不再回头改主意的语法,PCRE 系提供 |
| 线性时间 | 耗时与输入规模成正比,翻倍只带来常数倍增长 |
| 中位数 | 多轮测量里排在中间的那个值,比平均值更抗离群点 |
| 待验证项 | 本批未取得官方原文、因而在正文中只标注不引用具体数值的条目 |
写在最后:这篇用到的资料
写这篇文章时,我把同一段计时反复跑了几遍,确认真正稳定的不是毫秒数,而是最后一列的倍数;顺手把测法和判读表做成了一张能直接填的纸,也整理了几份配套的东西:
- 靶场环境对照表:DVWA、upload-labs 在 Windows / macOS / Linux 三平台的可行性与推荐路径
- Web 安全学习路线图:从基础打牢到安全管理,四个阶段各学什么
- 常用靶场清单:每个靶场练什么、适合哪个阶段
资料是我自己整理的,放在下面这个码上,扫码即可获取:
添加时备注「靶场」,优先通过。
拿到之后建议先看环境对照表那一份,先把「自己的正则跑在哪条引擎路线上」这件事确认下来,再回头改写法。