输入验证是在应用处理数据之前,对传入数据进行校验,检查其是否符合预期的格式、类型和取值范围的过程。根据 OWASP Input Validation Cheat Sheet (2025),缺少验证是大多数严重漏洞的根本原因。检查传入数据是第一道防御线,可防止错误或恶意数据进入系统。
要点
输入验证是检查来自用户、外部服务或其他组件的数据是否符预期标准的过程。这些标准包括数据类型(字符串、数字、日期)、格式(邮箱、URL、电话)、取值范围(年龄从18到120)、长度(密码从8到128个字符)以及允许的字符(仅拉丁字母、数字、连字符)。如果没有验证,应用可能会处理导致运行时错误、数据损坏或安全漏洞的数据。
缺少输入验证是SQL注入、XSS、命令注入、路径遍历和缓冲区溢出等漏洞的根本原因。根据MITRE CWE (2025),CWE-20(不正确的输入验证)在最危险的软件错误排名中位居第二。验证是纵深防御安全模型中的第一道防线:它会在错误数据到达系统其他组件之前将其拦截。
验证会拒绝不符合标准的数据。净化会通过删除或转义危险部分来修改数据。例如,在输入HTML内容时,验证可以检查文本长度,而净化可以通过HTML Purifier或DOMPurify库删除script标签。净化不能替代验证:两者协同工作。验证是“允许/禁止”策略,净化是“使用前已清理”。
验证按检查深度进行分类。格式验证最简单也最快,业务验证最复杂且最依赖上下文。所有三个层级都应依次应用:先格式,后语义,再业务逻辑。跳过任何层级都可能导致系统运行异常或产生漏洞。
| 层级 | 检查内容 | 示例 |
|---|---|---|
| 格式 | 数据类型、长度、正则表达式 | 邮箱包含@,长度5-100 |
| 语义 | 值的逻辑正确性 | 出生日期不在未来 |
| 业务验证 | 符合业务规则 | 转账金额不超过余额 |
检查数据类型、大小、格式和允许的字符。通过正则表达式、语言的內建类型和验证库实现。示例:UUID检查(8-4-4-4-12十六进制数字格式)、电话号码检查(仅数字、开头加号、7到15个字符)、整数检查(值在Integer.MIN_VALUE—Integer.MAX_VALUE范围内)。格式验证是每个输入字段的最低必要层级。
在业务领域上下文中检查数据的逻辑正确性。例如:开始日期不晚于结束日期、年龄在系统合理范围内、坐标在服务区域内。语义验证需要理解业务上下文,不能仅基于格式完成。示例:“门票数量”字段可能通过格式检查(整数,> 0),但在语义上不能超过空座数量。
最复杂的层级——检查数据是否符合应用的业务规则。示例:用户不能删除唯一的管理员、订单金额不超过信用额度、商品只有有库存时才能订购。业务验证通常需要数据库查询或外部服务,并在格式和语义检查之后执行。业务验证错误是用户不满的最常见原因。
客户端验证(在浏览器或移动应用中)是为了用户便利:无需向服务器发送数据即可获得即时反馈。然而,服务端验证是唯一可靠的,因为客户端代码总是可以被绕过。通过开发者工具、Postman或代理(Burp Suite)发送请求——客户端验证就不复存在。根据PortSwigger Research (2025),超过90%的受测网络应用在至少一个字段上完全依赖客户端验证。
客户端验证可以禁用发送按钮、高亮错误、显示提示。服务端验证是对每个参数的强制检查,即使客户端已检查过。在两个层级重复验证是标准做法。服务器应像客户端不存在一样检查数据。这可以保证免受修改请求、自动化攻击和恶意客户端的侵害。
在网页上——HTML5属性(required、pattern、min/max、type="email")和JavaScript。在移动应用中——原生文本字段验证器(Android中的InputFilter、iOS中的textField(:shouldChangeCharactersIn:))。React使用React Hook Form和Formik、Vue使用Vuelidate、Angular Reactive Forms——流行的客户端验证库。它们都支持自定义规则和异步验证(在服务器上检查登录名唯一性)。
// 使用Express和Joi进行服务端验证的示例
const Joi = require('joi');
const userSchema = Joi.object({
email: Joi.string()
.email()
.required()
.max(255),
age: Joi.number()
.integer()
.min(18)
.max(120)
.required(),
password: Joi.string()
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,128}$/)
.required()
});
app.post('/api/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) {
return res.status(400).json({
error: error.details[0].message
});
}
// value——已经过检查和安全的验证数据
const user = await User.create(value);
res.status(201).json(user);
});
移动应用对数据验证有特殊要求。屏幕更小——错误提示必须简洁,键盘要符合上下文(输入数字时用数字键盘),检查要异步进行以免阻塞界面。原生平台提供了应默认使用的內建验证机制。Android的Material Design Guidelines和iOS的Human Interface Guidelines包含关于显示验证错误的详细建议。
Jetpack Compose通过状态管理提供了声明式的验证方法。每个输入字段都与状态(MutableState)绑定,错误根据当前值计算。Compose Validator库简化了规则的创建:required、email、min/max length、pattern。验证在文本更改(onValueChange)或尝试提交表单时触发。建议仅在首次提交后或用户完成输入后(防抖300-500ms)显示错误。
SwiftUI没有內建的表单验证机制,但可以通过Combine和属性包装器轻松实现。对字段值使用@State,对错误使用计算属性。ValidatedPropertyKit框架提供现成的装饰器:@Validated().email()、@Validated().range(18...120)。iOS建议——使用键盘类型(UIKeyboardType.emailAddress、.numberPad)和自动大写,以减少输入层面的错误数量。
Flutter提供了Form和TextFormField类,通过validator回调进行內建验证。每个字段返回错误字符串,如果数据正确则返回null。FormState.validate()触发表单所有字段的检查。reactive_forms包用于复杂情况:自定义验证器、异步检查、动态规则。Flutter Web和移动版本使用相同的API,简化了维护。
// Flutter中表单验证的示例
Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Email is required';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
.hasMatch(value)) {
return 'Enter a valid email';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.validate()) {
// Process valid data
}
},
child: Text('Submit'),
),
],
),
)
现代框架提供了覆盖80%需求的內建验证器。其余20%需要自定义规则、正则表达式或现有规则的组合。关键原则——验证应该是声明式的,以便易于阅读、测试和维护。避免验证逻辑分散在控制器和页面中——将其提取到单独的类或模式中。
| 工具 | 平台 | 特点 |
|---|---|---|
| Joi | Node.js | 声明式模式、自定义消息 |
| Pydantic | Python | 类型提示、模型自动验证 |
| Zod | TypeScript | 类型推断、严格类型化 |
| javax.validation | Java | Bean Validation、@NotNull、@Size、@Pattern |
| FluentValidation | .NET | Fluent API、rulesets、条件规则 |
白名单——确定允许哪些数据,其余全部拒绝。黑名单——确定禁止哪些数据,其余全部放行。白名单总是更可靠:你确切知道哪些数据会通过。黑名单需要预见所有可能的攻击,这是不可能的。示例:检查年龄时使用白名单(仅18到120的数字),而不是黑名单(禁止“0”、“-1”、“999999”)。
正则表达式是格式验证的有效工具,但可能成为ReDoS攻击(正则表达式拒绝服务)的来源。某些模式(例如(a+)+b)会在长字符串上导致灾难性回溯,完全占用服务器CPU。请使用经过验证的正则库,并在应用正则表达式之前限制字符串长度。对于复杂情况(邮箱、URL),使用语言的內建解析器,而不是自行编写的正则表达式。
即使是经验丰富的开发人员在实现验证时也会犯错。最常见的有:仅客户端验证、过于严格的规则(密码“Must contain uppercase, lowercase, digit, special char, >= 12 chars, must not repeat characters”)、无信息量的错误消息("Error: invalid input")以及忽略边缘情况(开头/结尾空格、Unicode字符、空字符串)。这些错误中的每一个都会降低用户体验,并可能降低表单转化率。
if (value)不能区分空字符串与零、false或“0”最佳实践是建立由单元测试覆盖的集中式验证系统。每个规则都应单独测试:边界值、正确数据、典型攻击(SQLi尝试、XSS有效载荷、超长字符串)。验证的回归测试可防止在重构时意外削弱规则。使用基于属性的测试(QuickCheck、fast-check)生成随机数据,并检查验证不会因异常而崩溃。
常见问题
验证会拒绝不正确的数据,而净化会清理它们。例如,在输入HTML文本时,验证会检查最大长度,而净化会通过DOMPurify删除script标签。两个过程都是必需的:验证用于格式控制,净化用于输出安全。
不,永远不够。客户端验证很容易通过拦截和修改请求被绕过。使用Burp Suite等工具或直接使用curl。服务端验证是保护系统的唯一可靠方式。客户端验证仅用于改善用户体验。
通过文件签名验证检查MIME类型(不仅是扩展名)、文件大小和签名(文件开头的魔数)。永远不要信任扩展名——保存时重命名文件。对于图片,用服务器库(ImageMagick、Sharp)重新编码,这将从EXIF数据中删除嵌入的代码。
ReDoS(正则表达式拒绝服务)是一种攻击,攻击者发送特制的字符串,在正则表达式中引发灾难性回溯。结果是服务器CPU被100%占用,响应无法生成。防护:限制字符串长度、为正则设置超时、使用经过验证的模式。
是的,如果数据显示在WebView中或用于HTML上下文。如果后端被攻破,数据可能包含恶意代码。无论来源如何,都要验证和净化所有显示给用户的数据。在移动应用中,这对混合组件尤其重要。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。