Input validation
The Importance of Input Validation
Input validation is a critical step in ensuring application security and stability. Unprocessed user input can lead to SQL injection, cross-site scripting (XSS) attacks, or other security vulnerabilities. Node.js, as a server-side JavaScript runtime, requires special attention to strict validation of incoming data.
Basic Validation Methods
The simplest validation method involves checking whether the input exists and matches the expected type:
function validateUsername(username) {
if (!username || typeof username !== 'string') {
throw new Error('Username must be a non-empty string');
}
return username.trim();
}
For more complex scenarios, regular expressions can be used for pattern matching:
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new Error('Invalid email format');
}
return email;
}
Using Validation Libraries
Manually writing validation logic is error-prone and difficult to maintain. Popular validation libraries like Joi can simplify this process:
const Joi = require('joi');
const userSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(new RegExp('^[a-zA-Z0-9]{8,30}$')),
email: Joi.string().email({ minDomainSegments: 2 })
});
function validateUser(user) {
const { error, value } = userSchema.validate(user);
if (error) throw error;
return value;
}
Middleware Validation
In Express applications, validation middleware can be created to centralize input validation:
const validateLogin = (req, res, next) => {
const schema = Joi.object({
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
app.post('/login', validateLogin, (req, res) => {
// Handle login logic
});
File Upload Validation
File uploads require special handling, including checks for file type, size, etc.:
const multer = require('multer');
const upload = multer({
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
},
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
return cb(new Error('Only image files are allowed'));
}
cb(null, true);
}
});
app.post('/upload', upload.single('avatar'), (req, res) => {
// Handle uploaded file
});
Deep Object Validation
Nested objects require recursive validation:
const addressSchema = Joi.object({
street: Joi.string().required(),
city: Joi.string().required(),
zipCode: Joi.string().pattern(/^\d{5}(-\d{4})?$/)
});
const userSchema = Joi.object({
name: Joi.string().required(),
addresses: Joi.array().items(addressSchema)
});
Custom Validation Rules
Joi allows the creation of custom validation rules:
const Joi = require('joi');
const customValidation = Joi.extend((joi) => {
return {
type: 'string',
base: joi.string(),
rules: {
noSpaces: {
validate(value, helpers) {
if (/\s/.test(value)) {
return helpers.error('string.noSpaces');
}
return value;
}
}
},
messages: {
'string.noSpaces': '{{#label}} cannot contain spaces'
}
};
});
const schema = customValidation.string().noSpaces();
Validation Performance Optimization
For high-frequency validation scenarios, validation functions can be pre-compiled:
const Joi = require('joi');
const userSchema = Joi.object({
// Define schema
});
// Pre-compile
const validateUser = userSchema.validate.bind(userSchema);
// Use directly later
function processUser(user) {
const { error, value } = validateUser(user);
// ...
}
Client-Side and Server-Side Dual Validation
While client-side validation improves user experience, server-side validation is essential:
// Client-side validation example (React)
function UserForm() {
const [errors, setErrors] = useState({});
const validate = (values) => {
const errs = {};
if (!values.username) errs.username = 'Username is required';
if (values.password?.length < 8) errs.password = 'Password must be at least 8 characters';
setErrors(errs);
return Object.keys(errs).length === 0;
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate(formData)) {
// Submit to server
}
};
// Render form...
}
Validation Error Handling
Good error handling should provide clear error messages:
app.use((err, req, res, next) => {
if (err instanceof Joi.ValidationError) {
return res.status(422).json({
message: 'Validation failed',
details: err.details.map(d => ({
field: d.path.join('.'),
message: d.message
}))
});
}
next(err);
});
Database-Level Validation
In addition to application-layer validation, database constraints are important:
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
unique: true,
validate: {
validator: function(v) {
return /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/.test(v);
},
message: props => `${props.value} is not a valid email address`
}
},
// Other fields...
});
Separating Validation from Business Logic
Keeping validation logic independent aids code maintenance:
// validators/userValidator.js
exports.validateCreateUser = (user) => {
const schema = Joi.object({ /* ... */ });
return schema.validate(user);
};
// controllers/userController.js
const { validateCreateUser } = require('../validators/userValidator');
exports.createUser = async (req, res) => {
const { error, value } = validateCreateUser(req.body);
if (error) return res.status(400).json({ error: error.message });
// Business logic...
};
Testing Validation Logic
Write test cases for validation logic:
const { validateEmail } = require('./validators');
describe('Email validation', () => {
test('Accepts valid email', () => {
expect(validateEmail('test@example.com')).toBe('test@example.com');
});
test('Rejects invalid email', () => {
expect(() => validateEmail('invalid')).toThrow('Invalid email format');
});
});
Combining Validation with Type Systems
Using TypeScript can enhance validation:
interface User {
username: string;
email: string;
age?: number;
}
function validateUser(user: unknown): user is User {
// Runtime validation logic
return Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required(),
age: Joi.number().optional()
}).validate(user).error === undefined;
}
function processUser(user: unknown) {
if (!validateUser(user)) throw new Error('Invalid user data');
// Now user is inferred as the User interface
}
Validation and Security Headers
Setting appropriate security headers can prevent certain attacks:
const helmet = require('helmet');
app.use(helmet());
app.use(express.json({ limit: '10kb' })); // Limit request body size
Validation and Rate Limiting
Prevent brute-force attacks by limiting request frequency:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Limit each IP to 100 requests
});
app.use('/login', limiter);
Validation and Content Security Policy
Set CSP to prevent XSS attacks:
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:']
}
})
);
Validation and CSRF Protection
Prevent cross-site request forgery:
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});
app.post('/process', csrfProtection, (req, res) => {
// Validate CSRF token
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn