14、输入验证与净化:输入长度检查、字符集过滤、白名单与黑名单策略
说到输入验证,我脑子里第一个蹦出来的画面,是几年前一个深夜的线上故障。用户提交了一段看似正常的评论,结果后台数据库直接崩了。查了半天,发现是有人往输入框里塞了一段精心构造的 SQL 语句。嗯,从那以后,我对「用户输入」这四个字就格外敏感。
说白了,你永远不能相信用户输入的数据。这不是偏见,这是安全底线。今天我们就来聊聊,怎么给输入数据「上枷锁」。
为什么输入验证这么重要?
你想想看,你的程序就像一座城堡。用户输入就是城门。如果不设防,谁都能进来,那城堡里再好的防御也没用。
常见的攻击手法,比如 SQL 注入、XSS 攻击、缓冲区溢出,根源往往都在输入验证上。我见过太多新手,觉得「用户不会那么坏」,结果被现实狠狠教育了一顿。
核心原则: 信任边界。所有外部数据,包括用户输入、网络请求、文件读取,都必须经过验证和净化。
输入长度检查
这是最基础,也是最容易被忽略的一步。长度检查能防止缓冲区溢出,也能限制攻击者构造超长 payload。
我个人习惯,在接收任何字符串输入时,先做长度检查。比如用户名,我一般限制在 32 个字符以内。密码可以长一点,但也不能无限长。
// C 语言示例:安全地读取输入
#include <stdio.h>
#include <string.h>
#define MAX_USERNAME_LEN 32
#define BUFFER_SIZE 256
int safe_read_input(char *buffer, size_t max_len) {
if (fgets(buffer, max_len, stdin) == NULL) {
return -1; // 读取失败
}
// 去掉末尾的换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
// 检查是否被截断(输入过长)
if (strlen(buffer) == max_len - 1 && buffer[max_len-2] != '\n') {
// 输入过长,需要清空缓冲区
int c;
while ((c = getchar()) != '\n' && c != EOF);
return -2; // 输入过长
}
return 0;
}
int main() {
char username[BUFFER_SIZE];
printf("请输入用户名(最多 %d 个字符): ", MAX_USERNAME_LEN);
if (safe_read_input(username, MAX_USERNAME_LEN + 1) != 0) {
printf("输入无效!\n");
return 1;
}
printf("用户名: %s\n", username);
return 0;
}
小技巧: 长度检查要放在最前面。先检查长度,再做其他验证。这样可以避免处理超长字符串时出现性能问题。
字符集过滤
长度检查只是第一步。接下来,我们要看看输入里到底有什么字符。
字符集过滤,说白了就是只允许特定范围内的字符通过。比如用户名,我一般只允许字母、数字和下划线。其他字符,一律拒绝。
我在项目中遇到过一个问题:用户注册时,名字里带了特殊符号,结果在生成文件路径时出了问题。从那以后,我对字符集过滤就格外严格。
// 字符集过滤示例
#include <ctype.h>
#include <stdbool.h>
bool is_valid_username(const char *input) {
if (input == NULL) return false;
for (int i = 0; input[i] != '\0'; i++) {
// 只允许字母、数字、下划线
if (!isalnum(input[i]) && input[i] != '_') {
return false;
}
}
return true;
}
// 更严格的版本:只允许 ASCII 可见字符
bool is_ascii_printable(const char *input) {
if (input == NULL) return false;
for (int i = 0; input[i] != '\0'; i++) {
if (input[i] < 32 || input[i] > 126) {
return false;
}
}
return true;
}
注意: 字符集过滤不能只做一次。如果数据经过了编码转换(比如 URL 解码、Base64 解码),要在解码之后再做过滤。
白名单与黑名单策略
这是两种截然不同的思路。我强烈推荐白名单策略。
黑名单策略: 列出不允许的字符或模式。比如过滤掉 <script>、DROP TABLE 等。
白名单策略: 列出允许的字符或模式。其他的一律拒绝。
为什么我推荐白名单?因为黑名单永远有漏洞。攻击者总能找到你没想到的变种。比如你过滤了 <script>,人家用 <ScRiPt> 就绕过去了。你过滤了 DROP,人家用 DrOp 照样能跑。
白名单就简单多了。你只允许字母数字,那其他字符根本进不来。攻击者再厉害,也变不出花来。
| 策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 白名单 | 安全性高,不易绕过 | 可能限制合法输入 | 用户名、密码、路径 |
| 黑名单 | 灵活,允许大部分输入 | 容易遗漏,安全性低 | 日志过滤、简单文本处理 |
输入验证与净化的完整流程
下面这张图,是我自己总结的输入处理流程。每次写代码,我都会对照着走一遍。
实战中的避坑指南
我曾经接手过一个项目,代码里做了输入验证,但只在前端做了。后端直接信任了前端传过来的数据。结果呢?攻击者绕过前端,直接向后端发请求,数据就裸奔了。
记住:前端验证只是用户体验,后端验证才是安全底线。
还有一次,我看到有人用 strlen() 检查输入长度,但没考虑宽字符。用户输入了中文字符,一个中文字符占多个字节,长度检查就失效了。嗯,这里要注意,长度检查要基于实际存储的字节数,而不是字符数。
总结一下我的经验:
- 先做长度检查,再做内容检查
- 优先使用白名单策略
- 前端后端都要做验证
- 验证要在解码之后进行
- 拒绝非法输入,不要尝试修复
最后说一句:输入验证不是一劳永逸的事。攻击手法在进化,你的验证逻辑也要跟着更新。定期 review 你的验证代码,看看有没有遗漏的边界情况。
好了,这一章的内容就到这里。记住,安全无小事,从输入开始。