-
Notifications
You must be signed in to change notification settings - Fork 4
Input Validation Best Practices
Input validation is a critical security practice that involves verifying that data entering your application meets expected criteria before processing it. Proper input validation helps prevent a wide range of security vulnerabilities, including injection attacks, cross-site scripting (XSS), and other common attack vectors.
Unvalidated input is one of the most common sources of security vulnerabilities. Consider these scenarios:
- An application that accepts SQL queries directly from user input could be vulnerable to SQL injection
- A web form that renders user input without validation could enable cross-site scripting attacks
- An API that accepts unlimited file uploads without type checking could allow malicious file execution
Implementing robust input validation significantly reduces these risks.
Approach | Description | Recommendation |
---|---|---|
Whitelist | Accept only known good input | Preferred approach - more secure |
Blacklist | Reject known bad input | Less secure - attackers can bypass |
Example of Whitelist Validation (JavaScript):
// Whitelist validation - only accept alphanumeric usernames
function validateUsername(username) {
const alphanumericRegex = /^[a-zA-Z0-9]+$/;
return alphanumericRegex.test(username);
}
// Usage
if (!validateUsername(userInput)) {
return res.status(400).json({ error: 'Username must contain only letters and numbers' });
}
Blacklist Validation (Less Secure):
// Blacklist approach - try to block known bad characters
function validateInput(input) {
// This approach is incomplete and vulnerable to bypass
return !input.includes('<script>') && !input.includes('DROP TABLE');
}
Always implement validation on both sides:
Client-Side Validation:
- Provides immediate feedback to users
- Reduces server load
- Improves user experience
- Cannot be trusted for security
Server-Side Validation:
- Critical security control
- Cannot be bypassed by malicious users
- Must be implemented even if client-side validation exists
- Length constraints:
// Validate string length
function validateLength(input, min, max) {
return input.length >= min && input.length <= max;
}
// Usage
if (!validateLength(password, 8, 128)) {
return res.status(400).json({ error: 'Password must be between 8 and 128 characters' });
}
- Character set restrictions:
// Validate email format
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
// Validate alphanumeric with specific special characters
function validatePassword(password) {
const passwordRegex = /^[a-zA-Z0-9!@#$%^&*()-_=+]+$/;
return passwordRegex.test(password);
}
// Validate integer range
function validateInteger(value, min, max) {
const num = parseInt(value, 10);
return !isNaN(num) && Number.isInteger(num) && num >= min && num <= max;
}
// Validate decimal/float
function validateDecimal(value, min, max, precision) {
const num = parseFloat(value);
if (isNaN(num) || num < min || num > max) return false;
// Check decimal precision
const decimalPart = value.toString().split('.')[1] || '';
return decimalPart.length <= precision;
}
// Validate date format and range
function validateDate(dateString, minDate, maxDate) {
const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) return false;
// Check range
if (minDate && date < new Date(minDate)) return false;
if (maxDate && date > new Date(maxDate)) return false;
return true;
}
// Validate file type and size
function validateFile(file, allowedTypes, maxSizeInBytes) {
// Check file type
if (!allowedTypes.includes(file.mimetype)) {
return false;
}
// Check file size
if (file.size > maxSizeInBytes) {
return false;
}
return true;
}
// Usage
app.post('/upload', upload.single('document'), (req, res) => {
if (!validateFile(
req.file,
['application/pdf', 'image/jpeg', 'image/png'],
5 * 1024 * 1024 // 5MB
)) {
return res.status(400).json({ error: 'Invalid file type or size' });
}
// Process valid file
});
const { body, validationResult } = require('express-validator');
app.post('/register',
// Validation middleware
[
body('username')
.isAlphanumeric()
.isLength({ min: 3, max: 20 })
.withMessage('Username must be 3-20 alphanumeric characters'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must provide a valid email'),
body('password')
.isLength({ min: 8 })
.matches(/[a-z]/).withMessage('Password must include lowercase letter')
.matches(/[A-Z]/).withMessage('Password must include uppercase letter')
.matches(/[0-9]/).withMessage('Password must include a number')
.matches(/[^a-zA-Z0-9]/).withMessage('Password must include a special character')
],
// Handler
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process valid input
// ...
}
);
from django import forms
class UserRegistrationForm(forms.Form):
username = forms.CharField(
min_length=3,
max_length=20,
validators=[
RegexValidator(
regex=r'^[a-zA-Z0-9]+$',
message='Username must contain only letters and numbers'
)
]
)
email = forms.EmailField()
password = forms.CharField(
min_length=8,
widget=forms.PasswordInput,
validators=[
RegexValidator(
regex=r'^(?=.[a-z])(?=.[A-Z])(?=.\d)(?=.[@$!%?&])[A-Za-z\d@$!%?&]+$',
message='Password must include uppercase, lowercase, number and special character'
)
]
)
Validation should be paired with sanitization to ensure safe use of data.
// Using DOMPurify library
const DOMPurify = require('dompurify');
function sanitizeHtml(input) {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'target']
});
}
// Using with user input that will be displayed as HTML
const userComment = sanitizeHtml(req.body.comment);
// Using parameterized queries with PostgreSQL
const { Pool } = require('pg');
const pool = new Pool();
async function getUser(username) {
// Safe: Using parameterized query
const result = await pool.query(
'SELECT id, name, email FROM users WHERE username = $1',
[username]
);
return result.rows[0];
}
Vulnerability:
// Unsafe: Directly rendering user input
app.get('/profile', (req, res) => {
res.send(`<h1>Welcome, ${req.query.name}!</h1>`);
});
Mitigation:
const escapeHtml = require('escape-html');
app.get('/profile', (req, res) => {
// Escape HTML special characters
const safeName = escapeHtml(req.query.name || '');
res.send(<h1>Welcome, ${safeName}!</h1>
);
});
Vulnerability:
// Unsafe: Constructing SQL with string concatenation
function searchUsers(searchTerm) {
return db.query(`SELECT * FROM users WHERE username LIKE '%${searchTerm}%'`);
}
Mitigation:
// Safe: Using parameterized queries
function searchUsers(searchTerm) {
return db.query(
'SELECT * FROM users WHERE username LIKE $1',
[`%${searchTerm}%`]
);
}
Vulnerability:
// Unsafe: Directly using request body to update records
app.post('/users/:id', (req, res) => {
const userId = req.params.id;
db.updateUser(userId, req.body);
});
Mitigation:
// Safe: Explicitly selecting allowed fields
app.post('/users/:id', (req, res) => {
const userId = req.params.id;
// Only allow specific fields to be updated
const allowedUpdates = {
name: req.body.name,
email: req.body.email,
bio: req.body.bio
};
db.updateUser(userId, allowedUpdates);
});
- Implement server-side validation for ALL user inputs
- Use whitelist validation rather than blacklist
- Validate data type, format, length, and range
- Sanitize input before use in HTML, SQL, or other contexts
- Use parameterized queries for database operations
- Implement context-specific encoding for different output locations
- Add client-side validation for better user experience
- Log validation failures for security monitoring
- Use appropriate HTTP status codes for validation errors (400 Bad Request)
- Don't reveal too much information in validation error messages
- OWASP Input Validation Cheat Sheet
- OWASP XSS Prevention Cheat Sheet
- Express Validator Documentation
- Django Validation Documentation
Input validation is a critical security practice that involves verifying that data entering your application meets expected criteria before processing it. Proper input validation helps prevent a wide range of security vulnerabilities, including injection attacks, cross-site scripting (XSS), and other common attack vectors.
Unvalidated input is one of the most common sources of security vulnerabilities. Consider these scenarios:
- An application that accepts SQL queries directly from user input could be vulnerable to SQL injection
- A web form that renders user input without validation could enable cross-site scripting attacks
- An API that accepts unlimited file uploads without type checking could allow malicious file execution
Implementing robust input validation significantly reduces these risks.
Approach | Description | Recommendation |
---|---|---|
Whitelist | Accept only known good input | Preferred approach - more secure |
Blacklist | Reject known bad input | Less secure - attackers can bypass |
Example of Whitelist Validation (JavaScript):
// Whitelist validation - only accept alphanumeric usernames
function validateUsername(username) {
const alphanumericRegex = /^[a-zA-Z0-9]+$/;
return alphanumericRegex.test(username);
}
// Usage
if (!validateUsername(userInput)) {
return res.status(400).json({ error: 'Username must contain only letters and numbers' });
}
Blacklist Validation (Less Secure):
// Blacklist approach - try to block known bad characters
function validateInput(input) {
// This approach is incomplete and vulnerable to bypass
return !input.includes('<script>') && !input.includes('DROP TABLE');
}
Always implement validation on both sides:
Client-Side Validation:
- Provides immediate feedback to users
- Reduces server load
- Improves user experience
- Cannot be trusted for security
Server-Side Validation:
- Critical security control
- Cannot be bypassed by malicious users
- Must be implemented even if client-side validation exists
- Length constraints:
// Validate string length
function validateLength(input, min, max) {
return input.length >= min && input.length <= max;
}
// Usage
if (!validateLength(password, 8, 128)) {
return res.status(400).json({ error: 'Password must be between 8 and 128 characters' });
}
- Character set restrictions:
// Validate email format
function validateEmail(email) {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return emailRegex.test(email);
}
// Validate alphanumeric with specific special characters
function validatePassword(password) {
const passwordRegex = /^[a-zA-Z0-9!@#$%^&*()-_=+]+$/;
return passwordRegex.test(password);
}
// Validate integer range
function validateInteger(value, min, max) {
const num = parseInt(value, 10);
return !isNaN(num) && Number.isInteger(num) && num >= min && num <= max;
}
// Validate decimal/float
function validateDecimal(value, min, max, precision) {
const num = parseFloat(value);
if (isNaN(num) || num < min || num > max) return false;
// Check decimal precision
const decimalPart = value.toString().split('.')[1] || '';
return decimalPart.length <= precision;
}
// Validate date format and range
function validateDate(dateString, minDate, maxDate) {
const date = new Date(dateString);
// Check if date is valid
if (isNaN(date.getTime())) return false;
// Check range
if (minDate && date < new Date(minDate)) return false;
if (maxDate && date > new Date(maxDate)) return false;
return true;
}
// Validate file type and size
function validateFile(file, allowedTypes, maxSizeInBytes) {
// Check file type
if (!allowedTypes.includes(file.mimetype)) {
return false;
}
// Check file size
if (file.size > maxSizeInBytes) {
return false;
}
return true;
}
// Usage
app.post('/upload', upload.single('document'), (req, res) => {
if (!validateFile(
req.file,
['application/pdf', 'image/jpeg', 'image/png'],
5 * 1024 * 1024 // 5MB
)) {
return res.status(400).json({ error: 'Invalid file type or size' });
}
// Process valid file
});
const { body, validationResult } = require('express-validator');
app.post('/register',
// Validation middleware
[
body('username')
.isAlphanumeric()
.isLength({ min: 3, max: 20 })
.withMessage('Username must be 3-20 alphanumeric characters'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must provide a valid email'),
body('password')
.isLength({ min: 8 })
.matches(/[a-z]/).withMessage('Password must include lowercase letter')
.matches(/[A-Z]/).withMessage('Password must include uppercase letter')
.matches(/[0-9]/).withMessage('Password must include a number')
.matches(/[^a-zA-Z0-9]/).withMessage('Password must include a special character')
],
// Handler
(req, res) => {
// Check for validation errors
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Process valid input
// ...
}
);
from django import forms
class UserRegistrationForm(forms.Form):
username = forms.CharField(
min_length=3,
max_length=20,
validators=[
RegexValidator(
regex=r'^[a-zA-Z0-9]+$',
message='Username must contain only letters and numbers'
)
]
)
email = forms.EmailField()
password = forms.CharField(
min_length=8,
widget=forms.PasswordInput,
validators=[
RegexValidator(
regex=r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$',
message='Password must include uppercase, lowercase, number and special character'
)
]
)
Validation should be paired with sanitization to ensure safe use of data.
// Using DOMPurify library
const DOMPurify = require('dompurify');
function sanitizeHtml(input) {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p'],
ALLOWED_ATTR: ['href', 'target']
});
}
// Using with user input that will be displayed as HTML
const userComment = sanitizeHtml(req.body.comment);
// Using parameterized queries with PostgreSQL
const { Pool } = require('pg');
const pool = new Pool();
async function getUser(username) {
// Safe: Using parameterized query
const result = await pool.query(
'SELECT id, name, email FROM users WHERE username = $1',
[username]
);
return result.rows[0];
}
Vulnerability:
// Unsafe: Directly rendering user input
app.get('/profile', (req, res) => {
res.send(`<h1>Welcome, ${req.query.name}!</h1>`);
});
Mitigation:
const escapeHtml = require('escape-html');
app.get('/profile', (req, res) => {
// Escape HTML special characters
const safeName = escapeHtml(req.query.name || '');
res.send(`<h1>Welcome, ${safeName}!</h1>`);
});
Vulnerability:
// Unsafe: Constructing SQL with string concatenation
function searchUsers(searchTerm) {
return db.query(`SELECT * FROM users WHERE username LIKE '%${searchTerm}%'`);
}
Mitigation:
// Safe: Using parameterized queries
function searchUsers(searchTerm) {
return db.query(
'SELECT * FROM users WHERE username LIKE $1',
[`%${searchTerm}%`]
);
}
Vulnerability:
// Unsafe: Directly using request body to update records
app.post('/users/:id', (req, res) => {
const userId = req.params.id;
db.updateUser(userId, req.body);
});
Mitigation:
// Safe: Explicitly selecting allowed fields
app.post('/users/:id', (req, res) => {
const userId = req.params.id;
// Only allow specific fields to be updated
const allowedUpdates = {
name: req.body.name,
email: req.body.email,
bio: req.body.bio
};
db.updateUser(userId, allowedUpdates);
});
- Implement server-side validation for ALL user inputs
- Use whitelist validation rather than blacklist
- Validate data type, format, length, and range
- Sanitize input before use in HTML, SQL, or other contexts
- Use parameterized queries for database operations
- Implement context-specific encoding for different output locations
- Add client-side validation for better user experience
- Log validation failures for security monitoring
- Use appropriate HTTP status codes for validation errors (400 Bad Request)
- Don't reveal too much information in validation error messages
- [OWASP Input Validation Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)
- [OWASP XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html)
- [Express Validator Documentation](https://express-validator.github.io/)
- [Django Validation Documentation](https://docs.djangoproject.com/en/stable/ref/forms/validation/)