您现在的位置是:网站首页 > 数据验证与输入过滤文章详情
数据验证与输入过滤
陈川
【
Node.js
】
64365人已围观
12782字
数据验证与输入过滤的重要性
数据验证与输入过滤是Web应用安全的第一道防线。用户输入的数据永远不可信,恶意用户可能通过表单、URL参数或HTTP头注入攻击代码。Express框架提供了多种机制来验证和过滤输入数据,防止SQL注入、XSS攻击等安全威胁。
客户端验证与服务器端验证
客户端验证可以提高用户体验,但不能替代服务器端验证。攻击者可以轻松绕过客户端验证,直接向服务器发送恶意请求。
// 客户端验证示例
document.getElementById('registerForm').addEventListener('submit', function(e) {
const username = document.getElementById('username').value;
if (username.length < 4) {
alert('用户名至少4个字符');
e.preventDefault();
}
});
服务器端验证是必须的,Express中间件可以很好地处理:
app.post('/register', (req, res) => {
const { username } = req.body;
if (username.length < 4) {
return res.status(400).json({ error: '用户名至少4个字符' });
}
// 继续处理
});
Express验证中间件
express-validator
express-validator是基于validator.js的Express中间件,提供强大的验证功能:
const { body, validationResult } = require('express-validator');
app.post('/user',
body('username').isLength({ min: 4 }).trim().escape(),
body('email').isEmail().normalizeEmail(),
body('age').isInt({ min: 18, max: 120 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 安全的数据处理
}
);
Joi验证库
Joi提供了更丰富的验证模式:
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(4).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{8,30}$')),
email: Joi.string().email({ minDomainSegments: 2 })
});
app.post('/user', (req, res) => {
const { error, value } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details });
}
// 使用验证后的value
});
输入过滤技术
转义HTML
防止XSS攻击的基本方法是转义HTML特殊字符:
function escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
参数化查询
防止SQL注入的最佳实践是使用参数化查询:
// 使用mysql2库示例
const mysql = require('mysql2/promise');
async function getUser(id) {
const conn = await mysql.createConnection(/* 配置 */);
// 正确的参数化查询
const [rows] = await conn.execute('SELECT * FROM users WHERE id = ?', [id]);
return rows;
}
内容安全策略
设置CSP头可以限制资源加载,减少XSS风险:
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'", 'trusted.cdn.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'img.cdn.com']
}
}));
文件上传验证
文件上传需要特别验证,防止恶意文件上传:
const multer = require('multer');
const upload = multer({
limits: { fileSize: 1024 * 1024 * 5 }, // 5MB限制
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
return cb(new Error('只允许图片文件'));
}
cb(null, true);
}
});
app.post('/upload', upload.single('avatar'), (req, res) => {
// 处理上传文件
});
正则表达式验证
自定义正则表达式可以满足特定验证需求:
app.post('/product',
body('sku').matches(/^[A-Z]{3}-\d{4}$/),
body('price').isFloat({ min: 0.01, max: 9999.99 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理有效数据
}
);
自定义验证器
express-validator允许创建自定义验证器:
const { body } = require('express-validator');
const isStrongPassword = value => {
if (!value.match(/[A-Z]/) || !value.match(/[a-z]/) || !value.match(/[0-9]/)) {
throw new Error('密码必须包含大小写字母和数字');
}
return true;
};
app.post('/change-password',
body('newPassword').custom(isStrongPassword),
(req, res) => {
// 处理密码更改
}
);
批量验证中间件
可以创建可重用的验证中间件:
const validateUser = [
body('username').isLength({ min: 4 }).trim().escape(),
body('email').isEmail().normalizeEmail(),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
}
];
app.post('/register', validateUser, (req, res) => {
// 处理注册逻辑
});
验证错误处理
良好的错误处理可以提升用户体验:
app.post('/api/user', validateUser, (req, res) => {
// 处理逻辑
}, (err, req, res, next) => {
if (err instanceof ValidationError) {
return res.status(422).json({
message: '验证失败',
details: err.errors
});
}
next(err);
});
请求头验证
API请求通常需要验证头信息:
app.use('/api', (req, res, next) => {
const apiKey = req.get('X-API-Key');
if (!apiKey || apiKey !== process.env.API_KEY) {
return res.status(401).json({ error: '无效的API密钥' });
}
next();
});
速率限制
防止暴力破解攻击:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个IP限制100次请求
message: '请求过于频繁,请稍后再试'
});
app.use('/login', limiter);
数据规范化
规范化输入数据可以避免许多问题:
app.post('/search',
body('query').trim().normalizeEmail().toLowerCase(),
(req, res) => {
// 处理搜索逻辑
}
);
验证复杂数据结构
验证嵌套对象和数组:
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zip: Joi.string().regex(/^\d{5}(-\d{4})?$/)
});
const userSchema = Joi.object({
name: Joi.string().required(),
addresses: Joi.array().items(addressSchema).min(1)
});
app.post('/user', (req, res) => {
const { error, value } = userSchema.validate(req.body);
// 处理验证结果
});
验证内容类型
确保请求体是预期的内容类型:
app.use(express.json()); // 只接受JSON
app.use(express.urlencoded({ extended: true })); // 表单数据
// 自定义内容类型验证
app.use((req, res, next) => {
if (req.is('application/json')) {
next();
} else {
res.status(415).send('只支持JSON格式');
}
});
验证文件内容
验证上传文件的实际内容而不仅是扩展名:
const fileType = require('file-type');
app.post('/upload', upload.single('file'), async (req, res) => {
const buffer = req.file.buffer;
const type = await fileType.fromBuffer(buffer);
if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
return res.status(400).send('无效的图片格式');
}
// 处理有效文件
});
验证查询参数
URL查询参数也需要验证:
app.get('/products',
query('page').isInt({ min: 1 }).toInt(),
query('limit').optional().isInt({ min: 1, max: 100 }).toInt(),
query('sort').optional().isIn(['price', 'name', 'date']),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理查询
}
);
验证路由参数
动态路由参数验证:
app.get('/user/:id',
param('id').isMongoId(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理请求
}
);
验证Cookie和Session
验证客户端提供的Cookie:
app.use((req, res, next) => {
if (!req.cookies.sessionId || !isValidSession(req.cookies.sessionId)) {
return res.status(401).send('需要登录');
}
next();
});
验证JWT令牌
API常用的JWT验证:
const jwt = require('jsonwebtoken');
app.use('/api', (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).send('需要认证');
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch (err) {
res.status(401).send('无效的令牌');
}
});
验证重定向URL
防止开放重定向攻击:
app.get('/redirect', (req, res) => {
const url = req.query.url;
if (!isSafeRedirect(url)) {
return res.status(400).send('无效的重定向URL');
}
res.redirect(url);
});
function isSafeRedirect(url) {
const allowedDomains = ['example.com', 'trusted-site.org'];
try {
const domain = new URL(url).hostname;
return allowedDomains.includes(domain);
} catch {
return false;
}
}
验证内容长度
限制请求体大小防止DoS攻击:
app.use(express.json({ limit: '100kb' })); // 限制JSON大小
app.use(express.urlencoded({ limit: '100kb', extended: true }));
// 自定义内容长度检查
app.use((req, res, next) => {
const contentLength = parseInt(req.get('Content-Length'));
if (contentLength > 1024 * 1024) { // 1MB
return res.status(413).send('请求体过大');
}
next();
});
验证时间相关输入
日期和时间输入需要特别处理:
app.post('/event',
body('startDate').isISO8601().toDate(),
body('endDate').isISO8601().toDate()
.custom((value, { req }) => value > req.body.startDate)
.withMessage('结束日期必须晚于开始日期'),
(req, res) => {
// 处理验证结果
}
);
验证枚举值
确保输入是预定义的值之一:
const allowedStatuses = ['pending', 'approved', 'rejected'];
app.put('/order/:id/status',
body('status').isIn(allowedStatuses),
(req, res) => {
// 处理状态更新
}
);
验证相关字段
字段之间的关联验证:
app.post('/register',
body('password').isLength({ min: 8 }),
body('confirmPassword').custom((value, { req }) => {
if (value !== req.body.password) {
throw new Error('密码确认不匹配');
}
return true;
}),
(req, res) => {
// 处理注册
}
);
验证后清理数据
移除不必要的字段:
app.post('/user', validateUser, (req, res) => {
const userData = _.pick(req.body, ['username', 'email', 'password']);
// 只使用白名单字段
});
验证性能优化
大量数据验证时的性能考虑:
// 使用fastest-validator替代Joi处理大量数据
const Validator = require('fastest-validator');
const v = new Validator();
const schema = {
name: { type: 'string', min: 3, max: 255 },
age: { type: 'number', min: 18, max: 120 }
};
app.post('/bulk-users', (req, res) => {
const result = v.validate(req.body, schema);
if (result !== true) {
return res.status(400).json(result);
}
// 处理批量用户
});
验证与数据库交互
检查数据唯一性:
app.post('/user',
body('email').isEmail().custom(async email => {
const user = await User.findOne({ email });
if (user) {
throw new Error('邮箱已被注册');
}
return true;
}),
(req, res) => {
// 处理注册
}
);
验证与业务规则
结合业务逻辑的复杂验证:
app.post('/order',
body('items').isArray({ min: 1 }),
body('items.*.productId').isMongoId(),
body('items.*.quantity').isInt({ min: 1 }),
body('couponCode').optional().custom(async (code, { req }) => {
const coupon = await Coupon.findOne({ code });
if (!coupon || !coupon.isValid()) {
throw new Error('无效的优惠券');
}
req.validCoupon = coupon; // 附加验证结果到请求对象
return true;
}),
async (req, res) => {
// 处理订单,可以使用req.validCoupon
}
);
验证错误消息国际化
支持多语言的错误消息:
app.post('/user',
body('username')
.isLength({ min: 4 })
.withMessage(i18n.__('validation.username.minLength')),
body('email')
.isEmail()
.withMessage(i18n.__('validation.email.invalid')),
(req, res) => {
const errors = validationResult(req).formatWith(({ msg }) => msg);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理请求
}
);
验证与类型转换
自动转换输入数据类型:
app.get('/products',
query('page').default(1).toInt(),
query('limit').default(10).toInt(),
query('minPrice').optional().toFloat(),
query('maxPrice').optional().toFloat(),
query('inStock').optional().toBoolean(),
(req, res) => {
// 所有参数已转换为正确的类型
const { page, limit, minPrice, maxPrice, inStock } = req.query;
// 数据库查询
}
);
验证与日志记录
记录可疑的验证失败:
app.post('/login',
body('username').isLength({ min: 4 }),
body('password').isLength({ min: 8 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
// 记录失败的登录尝试
logSuspiciousActivity(req.ip, req.body);
return res.status(400).json({ errors: errors.array() });
}
// 处理登录
}
);
验证与速率限制结合
根据验证失败次数限制请求:
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: '尝试次数过多,请15分钟后再试',
skip: req => {
// 只有验证失败的请求才计数
const errors = validationResult(req);
return errors.isEmpty();
}
});
app.post('/login', loginLimiter, loginValidations, loginHandler);
验证与CSRF防护
结合CSRF令牌验证:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.post('/transfer',
csrfProtection,
body('amount').isFloat({ min: 0.01 }),
body('toAccount').isIBAN(),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理转账
}
);
验证与GraphQL
在GraphQL中实现输入验证:
const { ApolloServer } = require('apollo-server-express');
const typeDefs = gql`
input CreateUserInput {
username: String! @constraint(minLength: 4)
email: String! @constraint(format: "email")
}
type Mutation {
createUser(input: