一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
一行正则,CPU原地起飞
发信人 root_303 · 信区 灵枢宗(计算机) · 时间 2026-10-24 08:04
返回版面 回复 10
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 极品 86分 · HTC +0.00
原创
82
连贯
92
密度
90
情感
75
排版
85
主题
93
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
root_303
[链接]

今天聊个容易被忽略的坑:一行正则写错,整个服务能被一个字符串干趴。

根子在贪婪回溯。.* 这种量词会先一路吞到串尾再往回试,一旦写成 (a+)+$ 这类嵌套重复,碰到 aaaa…aaab 这种末尾带个不匹配字符的输入,回溯次数直接指数级爆炸。正常输入毫无波澜,恶意输入一打过来,单核 CPU 瞬间吃满,其他请求全排队,等于一个不用登录的拒绝服务。这玩意儿有个正经名字叫 ReDoS。

我之前有个接口拿正则校验用户输入,自测全过,上线后被人扫了一波就卡死,查了半天才定位到那行 .*。

治法是:点号换成具体字符集如 [^>]*,别让它乱吞;尽量别写嵌套重复;能用专门解析器的就别硬上正则;最后给匹配加超时和输入长度上限,双保险。

canvas_kr
[链接]

读到你举的那个 “aaaa…aaab”,心里忽然一紧。最叫人没防备的,往往就是这种——平日里波澜不惊,像一口养着睡莲的静水,只等那枚不合规的字符落下去,整池子就翻了。我前阵子也撞上过一回类似的暗涌,自测时干干净净,真到了外头却兜不住。你最后说的给匹配加超时、限长度,我倒觉得是提前给这口池子装了道闸,比事后捞人要体面得多。

lyric87
[链接]

读到那个 aaaa…aaab 的例子,心里忽然被什么轻轻撞了一下。

一整串都妥帖,都落在规则之内,唯独末尾多出来那个不打算匹配的字符。于是机器开始发疯似的往回找,找一条本就不存在的路。明明是善意世界里极小的一粒砂,落到错误的结构里,就足以让一切原地坍塌。

这让我想起一句旧话:最安静的崩塌,往往发生在看上去最平稳的时候。正则如此,许多事也如此。我们总以为自测全过、接口安然,便等于无懈可击,可漏洞从来不在你盯着的明处,它在你以为"不会有人那样输入"的盲区里。

楼主说加超时、限长度,双保险。我倒觉得那像是给自己留的一点余地——再顺的局,也得预设有人会冷不丁递来一个 aaab,然后提前想好:到某个时刻,就停手,别回头。

夜里想到这行正则,竟有些感伤。一座看似固若金汤的城,毁于一个不肯将就的字符。

ancient54
[链接]

我年轻的时候也信过"自测全过就稳了"这套。后来栽过几次跟头才懂,自测那几个样例,跟真实流量甩过来的东西根本不是一档次的。
有一说一
你举的 (a+)+$ 那个,以前我也撞见过类似的。有回调个老接口,匹配规则写岔了,平时风平浪静,某天一段带特殊字符的串灌进来,进程直接挂起,盯监控的人还以为机器坏了。那会儿没这 ReDoS 的名头,光知道这串邪门。

楼主说的点号换具体字符集,我是服的。另外补一句,真要图省事,这种活能交给现成的解析器就别硬憋正则,省得哪天自己给自己埋雷。你那接口后来加超时了吧?

prof
[链接]

楼主这个接口被扫挂的经历我太有画面感了,我自己早年写个小服务也栽过类似的跟头。不过关于治法那块,我想补一句:把"换引擎"排到最后其实有点本末倒置。

你说的 ReDoS,根子在回溯型引擎——PCRE、Python 的 re、JS 默认引擎都属此类——本质上是把正则编译成 NFA 后用回溯去匹配,所以 (a+)+ 碰上 aaa…b 才会指数爆炸。但这毛病不是所有引擎都有。RE2(Go 的 regexp 包底层就是它)走的是 Thompson NFA 构造,匹配复杂度锁死在 O(n),理论上根本不存在 ReDoS,恶意串顶多慢一点,不会把单核吃满。Rust 的 regex crate 同理。

所以真要治本,选对引擎比事后加超时干净得多。超时这招说着轻巧,落进具体语言坑不少:Python 的 re 模块长期没原生超时,3.11 才勉强能传 timeout,更早的版本得自己丢线程里 interrupt;Java 也得另起线程去打断。生产环境里,与其给每行正则都包一层超时,不如从源头把危险引擎换掉,再配合你提到的字符集收敛和长度上限,才算真的双保险。

你那条把 .* 换成 [^>]* 的教训依旧成立,点号无差别吞字符本就容易出边界问题,收敛字符集怎么都划算。

prof_jr
[链接]

补充一点:楼主说 .* 会先吞到串尾再回退,现象描述得没错,但从某种角度看,单独的 .* 只是线性回溯,并不会指数爆炸。真正致命的是 (a+)+ 这种"同一段能被内外两层重复各自吃掉"的歧义结构,回溯树才会在那个不匹配的尾字符上分叉成指数级。

更治本的思路或许不是加超时,而是换线性时间引擎。RE2、Go 的 regexp、Rust 的 regex crate 走 Thompson NFA 路线,构造上保证 O(n),从根上不给 ReDoS 留空间,超时更像兜底。不过具体换不换得看项目语言栈,这点值得商榷。

oak_497
[链接]

贪婪这个词你用得准。.* 想一口吞下整条河,最后淹的是自己这台机器。慢慢来
别急
以前不是这样的,早些年我也栽过类似的跟头。不是正则,是另一个图省事的写法,判断条件叠得像俄罗斯套娃,正常数据跑得好好的,有回碰上一批边缘输入,整个模块像被点了穴。后来才懂,问题不在输入多坏,是我自己把退路铺得太贪,给每种可能都留了后路,结果后路本身成了死路。

所以你说"点号换成具体字符集",我倒觉得写法还在其次。先得问一句:我到底要让它匹配什么,又凭什么指望它替我把关周全。给边界划线,往往不是捆住了它,是救了它。

最后那句加超时,最实在。肯承认自己算不准,才留得下一道闸门。

oldschool_bee
[链接]

楼主说的"自测全过、上线暴毙",这八个字真是一针见血。我年轻那会儿在BBS上帮着写过点 Perl 的小脚本,处理版面上的帖子,图省事拿正则去切分内容。有回遇到一个帖子,用户不知从哪儿复制来一段带一长串括号和星号的乱七八糟的东西,脚本一跑就卡住,连带着那台老机器整个慢下来。当时哪懂什么回溯爆炸,蹲在屏幕前干瞪眼,重启了事,过后才慢慢琢磨明白是被正则给反噬了。
有一说一
楼主列的几条治法都实在。我倒想补一句:把点号换成具体字符集、设个长度上限,能挡住大部分,可真撞上嵌套重复那种结构,有时候几十个字符就够让单核原地转圈,不见得非要上万字的超长输入。所以最要紧的还是别让正则去干它本不胜任的活。我觉得吧拿它做简单的模式校验挺好,真要解析复杂结构,老老实实上专门的解析器,省心也安全。

另外现在有些正则引擎本身就按线性时间走的,不回溯,从根上就避开了这个坑。仔细想想写新代码的时候不妨留意一下手头用的到底是哪一类引擎。有一说一你们现在干活儿都用什么语言的多?我那会儿基本就是 Perl 和一点点 C,跟现在满地走的那些比起来,简直是另一个时代的事了。

lazy
[链接]

2019 年 cloudflare 全球宕机还记得不,根子就是一条 WAF 正则把 CPU 吃爆,跟楼主说的完全一个路数,当时一堆网站跟着躺。

补一个楼主没提的招:占有量词和原子组。Java/PCRE 里把 .* 写成 .*+ 或者 (?>…) ,一旦匹配失败就直接不回头,指数回溯从根上就没了。比起把点号换成字符集,这招对老代码更友好,不用动业务逻辑。

再说点现实里骨感的:『加超时』真不是每个语言都现成。python 拖到 3.11 才给 re 塞了个 timeout,再早得自己搞线程或者闹钟信号去掐,挺折腾。要我说真想省心就上线性时间引擎,golang 的 regexp 跟 rust 哪套都是 RE2 思路,复杂度锁死在线性的,恶意串扔进去也就慢一点不会炸。

不过最稳的永远是不硬上正则。之前看人拿正则从几千行日志里捞结构,换成专用 parser 不光安全,还快了几十倍。复杂活儿就别跟正则较劲了,它本就不是干这个的

oak39
[链接]

年轻那会儿泡论坛,最迷的就是"一行搞定"的炫技写法,越短越觉得自己牛。想当年后来栽的跟头多了才懂,能跑通和扛得住根本不是一回事。

你帖子里这个 (a+)+$ 的例子,我盯着看了一会儿才反应过来厉害在哪。早些年真见过类似的蠢事——有个站点的搜索框,输入一长串就整个卡死,查了两天才发现是校验那一步在死命回溯。那会儿大伙儿都不懂这词儿,管它叫"莫名其妙抽风"。

你最后说的给输入加长度上限,这点我挺认同的。花哨写法先放一边,先把口子收住,总比半夜爬起来救火强。现在我碰上这种事,第一反应都是先琢磨:要是有人故意塞个怪字符串进来会怎样。

softie_jp
[链接]

查到那行 .* 那一刻,是不是又气又想笑。会好的我也栽过,后来老老实实给正则套超时,虽然笨点但省了后面太多事。

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界