输入验证与净化
所有外部输入都不可信,验证与净化是安全第一道防线。
验证原则
白名单优先
JavaScript
// 不安全:黑名单思维(易遗漏)
function validateUsername(username) {
const forbidden = ['<', '>', '"', "'", '&'];
for (const char of forbidden) {
if (username.includes(char)) return false;
}
return true;
}
// 安全:白名单验证
function validateUsername(username) {
// 只允许字母、数字、下划线,3-20字符
const validPattern = /^[a-zA-Z0-9_]{3,20}$/;
return validPattern.test(username);
}
严格类型检查
JavaScript
// 不安全:隐式类型转换
function processAge(age) {
if (age > 0 && age < 150) { // "20" 也会通过
return parseInt(age);
}
}
// 安全:严格类型验证
function processAge(age) {
if (typeof age !== 'number') {
throw new TypeError('年龄必须是数字');
}
if (!Number.isInteger(age) || age < 0 || age > 150) {
throw new RangeError('年龄必须是0-150之间的整数');
}
return age;
}
常见输入验证
字符串验证
JavaScript
const validators = {
// 邮箱验证
email(value) {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!pattern.test(value)) {
throw new Error('邮箱格式无效');
}
return value.toLowerCase();
},
// 手机号验证(中国大陆)
phone(value) {
const pattern = /^1[3-9]\d{9}$/;
if (!pattern.test(value)) {
throw new Error('手机号格式无效');
}
return value;
},
// URL验证
url(value) {
try {
const url = new URL(value);
if (!['http:', 'https:'].includes(url.protocol)) {
throw new Error('仅允许HTTP/HTTPS协议');
}
return url.href;
} catch {
throw new Error('URL格式无效');
}
},
// 用户名验证
username(value) {
if (typeof value !== 'string') {
throw new TypeError('用户名必须是字符串');
}
const trimmed = value.trim();
if (trimmed.length < 3 || trimmed.length > 20) {
throw new Error('用户名长度3-20字符');
}
if (!/^[a-zA-Z0-9_\u4e00-\u9fa5]+$/.test(trimmed)) {
throw new Error('用户名只能包含字母、数字、下划线、中文');
}
return trimmed;
}
};
数值验证
JavaScript
function validateNumber(value, options = {}) {
const {
min = -Infinity,
max = Infinity,
integer = false
} = options;
const num = Number(value);
if (!Number.isFinite(num)) {
throw new TypeError('必须是有效数字');
}
if (integer && !Number.isInteger(num)) {
throw new TypeError('必须是整数');
}
if (num < min || num > max) {
throw new RangeError(`必须在 ${min} 到 ${max} 之间`);
}
return integer ? Math.floor(num) : num;
}
// 使用示例
const age = validateNumber(input.age, { min: 0, max: 150, integer: true });
const price = validateNumber(input.price, { min: 0.01, max: 999999.99 });
数组验证
JavaScript
function validateArray(value, options = {}) {
const {
minLength = 0,
maxLength = Infinity,
itemValidator = null,
unique = false
} = options;
if (!Array.isArray(value)) {
throw new TypeError('必须是数组');
}
if (value.length < minLength || value.length > maxLength) {
throw new Error(`数组长度必须在 ${minLength} 到 ${maxLength} 之间`);
}
let items = value;
if (itemValidator) {
items = items.map((item, index) => {
try {
return itemValidator(item);
} catch (error) {
throw new Error(`第${index + 1}项: ${error.message}`);
}
});
}
if (unique) {
const seen = new Set();
for (const item of items) {
const key = JSON.stringify(item);
if (seen.has(key)) {
throw new Error('数组包含重复项');
}
seen.add(key);
}
}
return items;
}
// 使用示例
const tags = validateArray(input.tags, {
minLength: 1,
maxLength: 10,
itemValidator: v => {
if (typeof v !== 'string' || v.length > 20) {
throw new Error('标签必须为20字符内的字符串');
}
return v.trim();
}
});
对象验证
JavaScript
function validateObject(value, schema) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
throw new TypeError('必须是对象');
}
const result = {};
const errors = [];
for (const [key, validator] of Object.entries(schema)) {
const isRequired = key.endsWith('!');
const actualKey = isRequired ? key.slice(0, -1) : key;
if (!(actualKey in value)) {
if (isRequired) {
errors.push(`${actualKey}: 必填字段缺失`);
}
continue;
}
try {
result[actualKey] = validator(value[actualKey]);
} catch (error) {
errors.push(`${actualKey}: ${error.message}`);
}
}
// 检查未知字段
const allowedKeys = Object.keys(schema).map(k =>
k.endsWith('!') ? k.slice(0, -1) : k
);
for (const key of Object.keys(value)) {
if (!allowedKeys.includes(key)) {
errors.push(`${key}: 未知字段`);
}
}
if (errors.length > 0) {
throw new Error(errors.join('; '));
}
return result;
}
// 使用示例
const userSchema = {
'username!': validators.username,
'email!': validators.email,
'age': v => validateNumber(v, { min: 0, max: 150, integer: true })
};
const validUser = validateObject(input, userSchema);
数据净化
字符串净化
JavaScript
const sanitizers = {
// 移除HTML标签
stripHtml(value) {
return value.replace(/<[^>]*>/g, '');
},
// HTML实体编码
escapeHtml(value) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
return String(value).replace(/[&<>"'/]/g, c => map[c]);
},
// SQL特殊字符转义(基础)
escapeSql(value) {
return String(value).replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, c => {
switch (c) {
case '\0': return '\\0';
case '\x08': return '\\b';
case '\x09': return '\\t';
case '\x1a': return '\\z';
case '\n': return '\\n';
case '\r': return '\\r';
case '"':
case "'":
case '\\':
case '%': return '\\' + c;
default: return c;
}
});
},
// 文件名净化
sanitizeFilename(value) {
return String(value)
.replace(/[<>:"/\\|?*\x00-\x1f]/g, '') // 移除非法字符
.replace(/^\.+/, '') // 移除开头点
.replace(/\.+$/, '') // 移除结尾点
.slice(0, 255); // 限制长度
},
// 路径净化(防止路径遍历)
sanitizePath(value) {
const normalized = String(value)
.replace(/\.\./g, '') // 移除上级目录
.replace(/\/+/g, '/'); // 合并多余斜杠
// 确保在允许的目录内
const baseDir = '/uploads/';
const fullPath = baseDir + normalized.replace(/^\/+/, '');
if (!fullPath.startsWith(baseDir)) {
throw new Error('非法路径');
}
return fullPath;
}
};
对象净化
JavaScript
function sanitizeObject(obj, allowedKeys) {
if (typeof obj !== 'object' || obj === null) {
return {};
}
const result = {};
for (const key of allowedKeys) {
if (key in obj) {
result[key] = obj[key];
}
}
return result;
}
// 使用示例:只保留允许的字段
const cleanData = sanitizeObject(req.body, ['name', 'email', 'message']);
深度净化
JavaScript
function deepSanitize(value, options = {}) {
const { maxDepth = 10, maxLength = 10000 } = options;
function sanitize(item, depth) {
if (depth > maxDepth) {
throw new Error('数据层级过深');
}
if (typeof item === 'string') {
if (item.length > maxLength) {
throw new Error('字符串长度超出限制');
}
return sanitizers.escapeHtml(item.trim());
}
if (typeof item === 'number') {
if (!Number.isFinite(item)) {
throw new Error('无效数值');
}
return item;
}
if (typeof item === 'boolean') return item;
if (item === null) return null;
if (Array.isArray(item)) {
return item.map(v => sanitize(v, depth + 1));
}
if (typeof item === 'object') {
const result = {};
for (const [key, val] of Object.entries(item)) {
if (key.includes('__proto__') || key.includes('constructor')) {
continue; // 跳过危险键
}
result[key] = sanitize(val, depth + 1);
}
return result;
}
return undefined; // 其他类型移除
}
return sanitize(value, 0);
}
验证库推荐
JavaScript
// 使用Joi(Node.js)
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(0).max(150)
});
const { error, value } = schema.validate(input);
if (error) {
throw new Error(error.details[0].message);
}
JavaScript
// 使用Zod(TypeScript友好)
import { z } from 'zod';
const userSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional()
});
const validUser = userSchema.parse(input);
要点总结
| 验证类型 | 关键方法 | 注意事项 |
|---|---|---|
| 字符串 | 正则、长度、白名单 | 编码问题、Unicode |
| 数值 | 类型、范围、整数 | Infinity、NaN |
| 数组 | 长度、类型、唯一性 | 嵌套验证 |
| 对象 | Schema、字段过滤 | 原型污染防护 |
核心原则:
- 白名单优于黑名单
- 尽早验证,多层防御
- 验证与净化分离
- 使用成熟验证库
- 永远不信任外部输入
📝 发现内容有误?点击此处直接编辑