正则表达式安全:防止ReDoS攻击、限制匹配长度、超时控制、输入验证
正则表达式这东西,用好了是利器,用不好就是定时炸弹。我见过太多项目因为一个“看似完美”的正则,把整个服务拖垮了。今天咱们就聊聊正则的安全问题——说白了,就是怎么让你的正则既好用又不会成为系统瓶颈。
ReDoS攻击:正则表达式的“死循环”
ReDoS,全称是Regular Expression Denial of Service。什么意思呢?就是攻击者构造一个特殊的字符串,让你的正则引擎陷入“灾难性回溯”,CPU直接飙到100%。
为什么会这样?我举个例子你就明白了。
// 危险的正则:嵌套量词 + 重叠匹配
const regex = /^(a+)+b$/;
// 测试字符串: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac"
// 这个字符串没有匹配到b,但引擎会尝试所有可能的组合
// 时间复杂度从O(n)变成了O(2^n)
嗯,这里要注意。上面这个正则看起来人畜无害,但如果你传入一个很长的、不匹配的字符串,它就会疯狂回溯。我在项目中遇到过类似的情况——一个日志解析服务,因为一个正则写得不够严谨,线上CPU直接打满,排查了半天才发现是正则的问题。
(a+)+— 嵌套的重复量词(a|aa)+— 重叠的交替分支(.*)*— 贪婪匹配嵌套(a+)*b— 重复量词后跟非可选字符
如何防止ReDoS?三个实用策略
我个人习惯从三个层面来防范ReDoS。你想想看,这就像给正则表达式上了三道锁。
策略一:重写正则,消除回溯陷阱
最直接的办法,就是把容易引发灾难性回溯的模式改掉。说白了,就是让正则引擎“一眼就能看出结果”,不需要反复尝试。
// ❌ 危险写法
const badRegex = /^(a+)+b$/;
// ✅ 安全写法:去掉嵌套量词
const safeRegex = /^a+b$/;
// ❌ 危险写法
const badRegex2 = /^(\d+|\w+)+$/;
// ✅ 安全写法:使用原子组(如果支持)
const safeRegex2 = /^(?>\d+|\w+)+$/;
?>、PCRE的*+。这些特性告诉引擎“匹配过的就不要回溯了”。
策略二:限制匹配长度
我曾经接手过一个项目,用户可以在输入框里粘贴任意长度的文本,然后系统用正则去匹配。你猜怎么着?有人贴了一篇10万字的论文进去……
从那以后,我养成了一个习惯:任何正则匹配之前,先检查输入长度。
#include <stdio.h>
#include <string.h>
#include <regex.h>
#define MAX_INPUT_LEN 1024
int safe_regex_match(const char *pattern, const char *input) {
// 第一步:限制输入长度
if (strlen(input) > MAX_INPUT_LEN) {
fprintf(stderr, "输入过长,拒绝匹配\n");
return -1;
}
regex_t regex;
int ret = regcomp(®ex, pattern, REG_EXTENDED);
if (ret != 0) {
fprintf(stderr, "正则编译失败\n");
return -2;
}
ret = regexec(®ex, input, 0, NULL, 0);
regfree(®ex);
return ret;
}
策略三:超时控制
就算你限制了长度,有些正则还是可能跑很久。这时候就需要“定时炸弹拆除器”——超时控制。
在C语言里,没有内置的正则超时机制。我一般用两种方式实现:
| 方法 | 实现方式 | 适用场景 |
|---|---|---|
| 信号量(SIGALRM) | 设置定时器,超时发送信号中断匹配 | 单线程、简单场景 |
| 线程+超时 | 在子线程中执行匹配,主线程等待超时 | 多线程、生产环境 |
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <regex.h>
volatile int timeout_flag = 0;
void timeout_handler(int sig) {
timeout_flag = 1;
}
int regex_with_timeout(const char *pattern, const char *input, int seconds) {
// 设置超时信号
signal(SIGALRM, timeout_handler);
alarm(seconds);
regex_t regex;
regcomp(®ex, pattern, REG_EXTENDED);
int ret = regexec(®ex, input, 0, NULL, 0);
// 取消定时器
alarm(0);
if (timeout_flag) {
fprintf(stderr, "正则匹配超时!\n");
regfree(®ex);
return -3;
}
regfree(®ex);
return ret;
}
输入验证:第一道防线
其实,很多ReDoS攻击是可以从源头避免的。我的习惯是:永远不要信任用户输入。在正则匹配之前,先做一层输入验证。
验证什么?三个维度:
- 长度验证:设定合理的最大长度,比如邮箱不超过254字符
- 字符集验证:只允许预期的字符范围,比如只允许字母数字
- 格式预检:用简单的规则先过滤掉明显不合法的输入
// 输入验证示例
int validate_input(const char *input) {
// 1. 长度检查
if (strlen(input) > 256) return 0;
// 2. 字符集检查:只允许字母、数字、下划线、点、@
for (int i = 0; input[i] != '\0'; i++) {
if (!isalnum(input[i]) &&
input[i] != '_' &&
input[i] != '.' &&
input[i] != '@') {
return 0;
}
}
// 3. 格式预检:不能以特殊字符开头
if (input[0] == '.' || input[0] == '@') return 0;
return 1;
}
知识体系总览
下面这张图,是我整理的正则表达式安全防护体系。你可以把它当作一个检查清单。
实战中的避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
我曾经犯过的错:
- 坑1:在日志分析系统中,用了一个
(.*?)去匹配任意内容,结果用户日志里有一行特别长的数据,直接导致CPU 100%。后来我改成了[^,]+,问题解决。 - 坑2:没有做输入长度限制,用户传了一个1MB的字符串进来,正则跑了30秒才返回。从那以后,我所有正则匹配前都会先检查长度。
- 坑3:在信号处理函数里调用了非线程安全的函数,导致多线程环境下程序崩溃。记住:信号处理函数里只能做简单的事情。
嗯,正则安全这块,说白了就是“防人之心不可无”。你永远不知道用户会输入什么奇怪的东西。做好输入验证、优化正则写法、加上超时控制——这三板斧用好了,你的正则就能安安稳稳地跑在生产环境里。
1. 这个正则会不会因为一个不匹配的字符串而疯狂回溯?
2. 我有没有限制输入的长度?
3. 如果匹配时间超过1秒,系统会怎么样?
这三个问题想清楚了,ReDoS基本就防住了。