Appearance
输入验证
输入验证是网络安全的第一道防线,通过验证和清理用户输入,可以防止多种安全漏洞,包括注入攻击、跨站脚本(XSS)和缓冲区溢出等。
输入验证的重要性
为什么需要输入验证
所有来自外部的输入都应被视为潜在的恶意输入。未经验证的用户输入是许多安全漏洞的根本原因。
威胁模型
- 注入攻击 - SQL 注入、命令注入、LDAP 注入
- XSS 攻击 - 反射型、存储型、DOM 型 XSS
- 缓冲区溢出 - 长度验证不足导致
- 业务逻辑攻击 - 绕过业务规则
输入验证策略
1. 白名单验证(推荐)
白名单验证只接受已知安全的输入,拒绝所有其他输入。
javascript
// 白名单验证示例
function validateUserInput(input, fieldName) {
const validationRules = {
email: {
pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
maxLength: 254
},
username: {
pattern: /^[a-zA-Z0-9_]{3,20}$/,
maxLength: 20
},
phone: {
pattern: /^[\+]?[1-9][\d]{0,15}$/,
maxLength: 16
},
role: {
allowedValues: ['user', 'admin', 'moderator']
}
};
const rule = validationRules[fieldName];
if (!rule) {
throw new Error(`Unknown field: ${fieldName}`);
}
// 检查允许的值
if (rule.allowedValues) {
return rule.allowedValues.includes(input);
}
// 检查正则表达式
if (rule.pattern && typeof input === 'string') {
const isValid = rule.pattern.test(input);
const isLengthValid = input.length <= rule.maxLength;
return isValid && isLengthValid;
}
return false;
}
// 使用示例
app.post('/register', (req, res) => {
const { email, username, role } = req.body;
if (!validateUserInput(email, 'email')) {
return res.status(400).json({ error: 'Invalid email format' });
}
if (!validateUserInput(username, 'username')) {
return res.status(400).json({ error: 'Invalid username format' });
}
if (!validateUserInput(role, 'role')) {
return res.status(400).json({ error: 'Invalid role' });
}
// 继续处理注册
});
2. 黑名单验证
黑名单验证阻止已知的恶意输入,但不如白名单安全。
javascript
// 黑名单验证示例(不推荐单独使用)
function validateAgainstBlacklist(input) {
const blacklist = [
'<script', 'javascript:', 'vbscript:',
'onerror', 'onload', 'onclick',
'eval(', 'expression(', 'javascript'
];
const lowerInput = input.toLowerCase();
return !blacklist.some(item => lowerInput.includes(item));
}
// 更好的方式是结合使用白名单和黑名单
function enhancedValidateInput(input, type) {
// 首先使用白名单
if (!validateUserInput(input, type)) {
return false;
}
// 然后使用黑名单作为额外保护层
if (!validateAgainstBlacklist(input)) {
return false;
}
return true;
}
数据类型验证
基础数据类型验证
javascript
// 验证数字
function validateNumber(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
const num = Number(value);
if (isNaN(num)) {
return { valid: false, error: 'Value must be a number' };
}
if (num < min || num > max) {
return { valid: false, error: `Value must be between ${min} and ${max}` };
}
return { valid: true, value: num };
}
// 验证整数
function validateInteger(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
const num = Number(value);
if (!Number.isInteger(num)) {
return { valid: false, error: 'Value must be an integer' };
}
if (num < min || num > max) {
return { valid: false, error: `Value must be between ${min} and ${max}` };
}
return { valid: true, value: num };
}
// 验证布尔值
function validateBoolean(value) {
if (typeof value === 'boolean') {
return { valid: true, value };
}
if (typeof value === 'string') {
const lowerValue = value.toLowerCase();
if (['true', '1', 'yes', 'on'].includes(lowerValue)) {
return { valid: true, value: true };
}
if (['false', '0', 'no', 'off'].includes(lowerValue)) {
return { valid: true, value: false };
}
}
return { valid: false, error: 'Value must be a boolean' };
}
// 验证日期
function validateDate(value) {
const date = new Date(value);
if (isNaN(date.getTime())) {
return { valid: false, error: 'Invalid date format' };
}
return { valid: true, value: date.toISOString() };
}
数组和对象验证
javascript
// 验证数组
function validateArray(value, itemValidator, options = {}) {
const { minItems = 0, maxItems = Infinity, unique = false } = options;
if (!Array.isArray(value)) {
return { valid: false, error: 'Value must be an array' };
}
if (value.length < minItems || value.length > maxItems) {
return {
valid: false,
error: `Array must have between ${minItems} and ${maxItems} items`
};
}
if (unique && new Set(value).size !== value.length) {
return { valid: false, error: 'Array must contain unique items' };
}
for (let i = 0; i < value.length; i++) {
const itemValidation = itemValidator(value[i]);
if (!itemValidation.valid) {
return {
valid: false,
error: `Item at index ${i} is invalid: ${itemValidation.error}`
};
}
}
return { valid: true, value };
}
// 验证对象
function validateObject(value, schema) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return { valid: false, error: 'Value must be an object' };
}
const errors = [];
for (const [field, validator] of Object.entries(schema)) {
const fieldValue = value[field];
if (validator.required && fieldValue === undefined) {
errors.push(`${field} is required`);
continue;
}
if (fieldValue !== undefined) {
const validation = validator.validate(fieldValue);
if (!validation.valid) {
errors.push(`${field}: ${validation.error}`);
}
}
}
if (errors.length > 0) {
return { valid: false, error: errors.join('; ') };
}
return { valid: true, value };
}
// 使用示例
const userSchema = {
name: {
required: true,
validate: (value) => validateString(value, { minLength: 1, maxLength: 50 })
},
age: {
required: true,
validate: (value) => validateInteger(value, 0, 120)
},
emails: {
required: false,
validate: (value) => validateArray(value,
(item) => validateUserInput(item, 'email'),
{ maxItems: 5 }
)
}
};
长度和范围验证
字符串长度验证
javascript
// 验证字符串长度和内容
function validateString(value, options = {}) {
const {
minLength = 0,
maxLength = Infinity,
allowEmpty = false,
trim = true
} = options;
if (typeof value !== 'string') {
return { valid: false, error: 'Value must be a string' };
}
if (trim) {
value = value.trim();
}
if (!allowEmpty && value.length === 0) {
return { valid: false, error: 'Value cannot be empty' };
}
if (value.length < minLength) {
return { valid: false, error: `Value must be at least ${minLength} characters` };
}
if (value.length > maxLength) {
return { valid: false, error: `Value must be no more than ${maxLength} characters` };
}
return { valid: true, value };
}
// 特殊字符验证
function validateNoSpecialChars(value, allowedChars = []) {
// 默认不允许的特殊字符
const disallowedRegex = /[^a-zA-Z0-9\s\-_.~]/;
const disallowedChars = value.match(disallowedRegex);
if (disallowedChars && !disallowedChars.every(char => allowedChars.includes(char))) {
return {
valid: false,
error: 'Value contains disallowed special characters'
};
}
return { valid: true, value };
}
文件上传验证
javascript
const multer = require('multer');
const path = require('path');
// 验证文件类型和大小
function createFileFilter(allowedTypes, maxSize) {
return (req, file, cb) => {
// 验证文件类型
const ext = path.extname(file.originalname).toLowerCase();
const mimeType = file.mimetype;
const isValidType = allowedTypes.some(type => {
if (type.startsWith('.')) {
// 检查扩展名
return ext === type.toLowerCase();
} else {
// 检查 MIME 类型
return mimeType.startsWith(type);
}
});
if (!isValidType) {
return cb(new Error('Invalid file type'), false);
}
// 验证文件大小
if (file.size > maxSize) {
return cb(new Error(`File size exceeds ${maxSize} bytes`), false);
}
cb(null, true);
};
}
// 验证文件名
function sanitizeFileName(filename) {
// 移除危险字符
return filename
.replace(/[^a-zA-Z0-9.-]/g, '_') // 替换特殊字符
.replace(/\.\.+/, '.') // 防止路径遍历
.replace(/^\.|[\/\\:*?"<>|]+$/, ''); // 防止危险字符
}
// 配置 multer
const upload = multer({
dest: 'uploads/',
fileFilter: createFileFilter(['.jpg', '.jpeg', '.png', '.gif', 'image/'], 5 * 1024 * 1024), // 5MB
limits: { fileSize: 5 * 1024 * 1024 }
});
服务器端验证实现
Express.js 验证中间件
javascript
// 通用验证中间件
function validationMiddleware(validations) {
return (req, res, next) => {
const errors = [];
for (const [field, validators] of Object.entries(validations)) {
const value = getNestedValue(req, field); // 支持嵌套字段
for (const validator of validators) {
const result = validator(value);
if (!result.valid) {
errors.push({
field,
value: value,
error: result.error
});
break; // 找到第一个错误就停止验证此字段
}
}
}
if (errors.length > 0) {
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
next();
};
}
// 辅助函数:获取嵌套对象的值
function getNestedValue(obj, path) {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : undefined;
}, obj);
}
// 使用验证中间件
const registerValidations = {
'user.email': [
(value) => validateString(value, { maxLength: 254 }),
(value) => validateUserInput(value, 'email')
],
'user.username': [
(value) => validateString(value, { minLength: 3, maxLength: 20 }),
(value) => validateUserInput(value, 'username')
],
'user.password': [
(value) => validateString(value, { minLength: 8, maxLength: 128 }),
(value) => validatePasswordStrength(value) // 假设有这个函数
],
'user.age': [
(value) => validateInteger(value, 13, 120)
]
};
app.post('/register', validationMiddleware(registerValidations), (req, res) => {
// 验证通过,继续处理
res.json({ success: true });
});
Joi 验证库示例
javascript
const Joi = require('joi');
// 定义验证模式
const userSchema = Joi.object({
email: Joi.string()
.email({ tlds: { allow: false } }) // 不允许顶级域名
.max(254)
.required(),
username: Joi.string()
.alphanum()
.min(3)
.max(20)
.pattern(new RegExp('^[a-zA-Z0-9_]*$'))
.required(),
password: Joi.string()
.min(8)
.max(128)
.pattern(new RegExp('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])'))
.required(),
age: Joi.number()
.integer()
.min(13)
.max(120)
.required(),
role: Joi.string()
.valid('user', 'admin', 'moderator')
.default('user'),
preferences: Joi.object({
newsletter: Joi.boolean().default(false),
notifications: Joi.boolean().default(true)
}).default({})
});
// 验证中间件
function joiValidation(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // 返回所有错误,不只是第一个
stripUnknown: true // 移除未知字段
});
if (error) {
const errors = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
req.validatedBody = value;
next();
};
}
// 使用 Joi 验证
app.post('/register', joiValidation(userSchema), (req, res) => {
const userData = req.validatedBody;
// 使用验证后的数据
res.json({ success: true, user: userData });
});
客户端和服务端双重验证
客户端验证
html
<!DOCTYPE html>
<html>
<head>
<title>Input Validation Example</title>
<style>
.error { color: red; font-size: 12px; }
.valid { border: 2px solid green; }
.invalid { border: 2px solid red; }
.field-container { margin: 10px 0; }
</style>
</head>
<body>
<form id="registrationForm">
<div class="field-container">
<label for="email">Email:</label>
<input type="email" id="email" name="email" required>
<div id="email-error" class="error"></div>
</div>
<div class="field-container">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<div id="username-error" class="error"></div>
</div>
<div class="field-container">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<div id="password-error" class="error"></div>
</div>
<button type="submit">Register</button>
</form>
<script>
// 客户端验证类
class FormValidator {
constructor(formId) {
this.form = document.getElementById(formId);
this.setupEventListeners();
}
setupEventListeners() {
// 实时验证
const inputs = this.form.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('blur', () => this.validateField(input));
input.addEventListener('input', () => this.clearError(input));
});
// 表单提交验证
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
validateField(input) {
const fieldName = input.name;
const value = input.value.trim();
// 清除之前的错误
this.clearError(input);
// 根据字段类型验证
let result = this.validateByType(fieldName, value);
if (!result.valid) {
this.showError(input, result.error);
return false;
}
this.showSuccess(input);
return true;
}
validateByType(fieldName, value) {
switch (fieldName) {
case 'email':
return this.validateEmail(value);
case 'username':
return this.validateUsername(value);
case 'password':
return this.validatePassword(value);
default:
return { valid: true };
}
}
validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!email) {
return { valid: false, error: 'Email is required' };
}
if (!emailRegex.test(email)) {
return { valid: false, error: 'Please enter a valid email address' };
}
if (email.length > 254) {
return { valid: false, error: 'Email is too long' };
}
return { valid: true };
}
validateUsername(username) {
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
if (!username) {
return { valid: false, error: 'Username is required' };
}
if (!usernameRegex.test(username)) {
return {
valid: false,
error: 'Username must be 3-20 characters, letters, numbers, and underscores only'
};
}
return { valid: true };
}
validatePassword(password) {
if (!password) {
return { valid: false, error: 'Password is required' };
}
if (password.length < 8) {
return { valid: false, error: 'Password must be at least 8 characters' };
}
if (!/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])/.test(password)) {
return {
valid: false,
error: 'Password must contain uppercase, lowercase, number, and special character'
};
}
return { valid: true };
}
showError(input, message) {
input.classList.remove('valid');
input.classList.add('invalid');
const errorElement = document.getElementById(`${input.name}-error`);
if (errorElement) {
errorElement.textContent = message;
}
}
showSuccess(input) {
input.classList.remove('invalid');
input.classList.add('valid');
}
clearError(input) {
input.classList.remove('invalid', 'valid');
const errorElement = document.getElementById(`${input.name}-error`);
if (errorElement) {
errorElement.textContent = '';
}
}
async handleSubmit(event) {
event.preventDefault();
// 验证所有字段
let isValid = true;
const inputs = this.form.querySelectorAll('input');
inputs.forEach(input => {
if (!this.validateField(input)) {
isValid = false;
}
});
if (isValid) {
// 提交表单到服务器
this.submitForm();
}
}
async submitForm() {
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
try {
const response = await fetch('/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (response.ok) {
alert('Registration successful!');
} else {
alert('Registration failed: ' + result.error);
}
} catch (error) {
alert('Network error occurred');
}
}
}
// 初始化验证器
document.addEventListener('DOMContentLoaded', () => {
new FormValidator('registrationForm');
});
</script>
</body>
</html>
特殊场景验证
URL 和路径验证
javascript
// 验证 URL
function validateUrl(url, options = {}) {
const { protocols = ['http', 'https'], requireTld = true } = options;
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol.slice(0, -1))) {
return { valid: false, error: `Protocol must be one of: ${protocols.join(', ')}` };
}
if (requireTld && !/\.[a-zA-Z]{2,}/.test(parsedUrl.hostname)) {
return { valid: false, error: 'Invalid domain name' };
}
return { valid: true, value: parsedUrl.href };
} catch (error) {
return { valid: false, error: 'Invalid URL format' };
}
}
// 验证路径(防止路径遍历)
function validatePath(pathStr, allowedBaseDir) {
const normalizedPath = path.normalize(pathStr);
const resolvedPath = path.resolve(normalizedPath);
const allowedPath = path.resolve(allowedBaseDir);
if (!resolvedPath.startsWith(allowedPath)) {
return { valid: false, error: 'Path traversal detected' };
}
return { valid: true, value: resolvedPath };
}
JSON 数据验证
javascript
// 验证 JSON 数据结构
function validateJsonStructure(jsonStr, schema) {
try {
const data = JSON.parse(jsonStr);
return validateObject(data, schema);
} catch (error) {
return { valid: false, error: 'Invalid JSON format' };
}
}
// 安全的 JSON 解析(防止原型污染)
function safeJsonParse(jsonStr) {
// 防止原型污染
return JSON.parse(jsonStr, (key, value) => {
if (key === '__proto__' || key === 'constructor') {
throw new Error('Prototype pollution attempt detected');
}
return value;
});
}
验证错误处理
统一错误格式
javascript
// 统一验证错误处理
class ValidationError extends Error {
constructor(field, message, value) {
super(message);
this.field = field;
this.message = message;
this.value = value;
this.name = 'ValidationError';
}
}
// 错误处理中间件
function errorHandler(err, req, res, next) {
if (err instanceof ValidationError) {
return res.status(400).json({
error: 'Validation Error',
details: [{
field: err.field,
message: err.message,
value: err.value
}]
});
}
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation Failed',
details: err.details?.map(detail => ({
field: detail.context?.key || detail.path,
message: detail.message,
value: detail.context?.value
})) || []
});
}
// 其他错误
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal Server Error' });
}
app.use(errorHandler);
性能优化
验证缓存
javascript
// 验证结果缓存
class ValidationCache {
constructor(ttl = 300000) { // 5分钟 TTL
this.cache = new Map();
this.ttl = ttl;
}
get(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.value;
}
this.cache.delete(key);
return null;
}
set(key, value) {
this.cache.set(key, {
value,
timestamp: Date.now()
});
}
clear() {
this.cache.clear();
}
}
const validationCache = new ValidationCache();
// 带缓存的验证函数
function cachedValidateEmail(email) {
const cacheKey = `email:${email}`;
const cached = validationCache.get(cacheKey);
if (cached !== null) {
return cached;
}
const result = validateUserInput(email, 'email');
validationCache.set(cacheKey, result);
return result;
}
最佳实践
1. 分层验证
javascript
// 客户端、服务端、数据库多层验证
const validationLayers = {
// 第一层:客户端验证(用户体验)
client: {
instantFeedback: true,
basicChecks: ['required', 'format', 'length']
},
// 第二层:服务端验证(安全关键)
server: {
deepValidation: true,
businessRules: true,
sanitization: true
},
// 第三层:数据库约束
database: {
constraints: ['NOT NULL', 'CHECK', 'FOREIGN KEY'],
triggers: ['validation_triggers']
}
};
2. 验证策略配置
javascript
// 验证策略配置
const validationConfig = {
// 开发环境:详细错误信息
development: {
showStack: true,
detailedErrors: true
},
// 生产环境:最小错误信息
production: {
showStack: false,
genericErrors: true,
rateLimiting: true
},
// 敏感操作:加强验证
sensitiveOperations: {
requireMfa: true,
ipWhitelist: true,
additionalValidation: true
}
};
总结
输入验证是网络安全的基石,应该:
- 始终验证所有外部输入
- 优先使用白名单验证
- 在客户端和服务端都实施验证
- 实施适当的错误处理
- 定期审查和更新验证规则
- 考虑性能影响
- 遵循最小权限原则