Skip to content
On this page

输入验证

输入验证是网络安全的第一道防线,通过验证和清理用户输入,可以防止多种安全漏洞,包括注入攻击、跨站脚本(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
  }
};

总结

输入验证是网络安全的基石,应该:

  1. 始终验证所有外部输入
  2. 优先使用白名单验证
  3. 在客户端和服务端都实施验证
  4. 实施适当的错误处理
  5. 定期审查和更新验证规则
  6. 考虑性能影响
  7. 遵循最小权限原则