全部学科
Python全栈
python
NodeJS全栈
nodejs
小程序首页
📅 2026-05-16 10 分钟 ✍️ juanwangdev

输入验证与净化

所有外部输入都不可信,验证与净化是安全第一道防线。

验证原则

白名单优先

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 = {
      '&': '&amp;',
      '<': '&lt;',
      '>': '&gt;',
      '"': '&quot;',
      "'": '&#x27;',
      '/': '&#x2F;'
    };
    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、字段过滤原型污染防护

核心原则

  • 白名单优于黑名单
  • 尽早验证,多层防御
  • 验证与净化分离
  • 使用成熟验证库
  • 永远不信任外部输入

📝 发现内容有误?点击此处直接编辑

← 上一篇 跨站请求伪造(CSRF)防御
下一篇 → 输出编码与转义
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库