Input validation and sanitization are critical security practices. This article covers validation techniques, sanitization methods, and secure input handling.
Introduction
Input validation and sanitization provide:
- Security protection
- Data integrity
- Error prevention
- User experience
- Compliance
Understanding these practices helps you:
- Prevent security vulnerabilities
- Validate user input
- Sanitize dangerous content
- Protect applications
- Build secure systems
Input Validation
Basic Validation
// ✅ Good: Basic input validation
function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
function validatePassword(password) {
return password.length >= 8;
}
function validateUsername(username) {
return /^[a-zA-Z0-9_]{3,20}$/.test(username);
}
// Usage
console.log(validateEmail('[email protected]')); // true
console.log(validateEmail('invalid')); // false
console.log(validatePassword('short')); // false
console.log(validatePassword('securePass123')); // true
Type Validation
// ✅ Good: Type validation
function validateType(value, expectedType) {
switch (expectedType) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'array':
return Array.isArray(value);
case 'object':
return value !== null && typeof value === 'object' && !Array.isArray(value);
case 'email':
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
case 'url':
try {
new URL(value);
return true;
} catch {
return false;
}
default:
return false;
}
}
// Usage
console.log(validateType('hello', 'string')); // true
console.log(validateType(42, 'number')); // true
console.log(validateType('[email protected]', 'email')); // true
console.log(validateType('https://example.com', 'url')); // true
Range Validation
// ✅ Good: Range validation
function validateRange(value, min, max) {
return value >= min && value <= max;
}
function validateLength(str, minLength, maxLength) {
return str.length >= minLength && str.length <= maxLength;
}
function validateAge(age) {
return validateRange(age, 0, 150);
}
// Usage
console.log(validateRange(50, 0, 100)); // true
console.log(validateLength('hello', 3, 10)); // true
console.log(validateAge(25)); // true
console.log(validateAge(200)); // false
Comprehensive Validator
// ✅ Good: Comprehensive validator
class Validator {
static rules = {
required: (value) => value !== null && value !== undefined && value !== '',
email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
minLength: (value, min) => value.length >= min,
maxLength: (value, max) => value.length <= max,
pattern: (value, pattern) => pattern.test(value),
number: (value) => !isNaN(value) && typeof value === 'number',
url: (value) => {
try {
new URL(value);
return true;
} catch {
return false;
}
}
};
static validate(data, schema) {
const errors = {};
for (const [field, rules] of Object.entries(schema)) {
const value = data[field];
for (const [ruleName, ruleValue] of Object.entries(rules)) {
const rule = this.rules[ruleName];
if (!rule) {
console.warn(`Unknown rule: ${ruleName}`);
continue;
}
const isValid = Array.isArray(ruleValue)
? rule(value, ...ruleValue)
: rule(value, ruleValue);
if (!isValid) {
if (!errors[field]) {
errors[field] = [];
}
errors[field].push(ruleName);
}
}
}
return {
valid: Object.keys(errors).length === 0,
errors
};
}
}
// Usage
const schema = {
email: { required: true, email: true },
password: { required: true, minLength: [8] },
username: { required: true, minLength: [3], maxLength: [20] }
};
const data = {
email: '[email protected]',
password: 'securePass123',
username: 'john_doe'
};
const result = Validator.validate(data, schema);
console.log(result); // { valid: true, errors: {} }
Input Sanitization
HTML Sanitization
// ✅ Good: HTML sanitization
function sanitizeHTML(html) {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
}
function escapeHTML(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, (char) => map[char]);
}
// Usage
const userInput = '<script>alert("XSS")</script>';
console.log(sanitizeHTML(userInput));
// <script>alert("XSS")</script>
console.log(escapeHTML(userInput));
// <script>alert("XSS")</script>
String Sanitization
// ✅ Good: String sanitization
function sanitizeString(str) {
return str
.trim()
.replace(/\s+/g, ' ')
.replace(/[<>]/g, '');
}
function removeSpecialChars(str) {
return str.replace(/[^a-zA-Z0-9\s]/g, '');
}
function sanitizeFilename(filename) {
return filename
.replace(/[^a-zA-Z0-9._-]/g, '')
.substring(0, 255);
}
// Usage
console.log(sanitizeString(' hello world ')); // 'hello world'
console.log(removeSpecialChars('hello@world!')); // 'helloworld'
console.log(sanitizeFilename('my<file>.txt')); // 'myfile.txt'
URL Sanitization
// ✅ Good: URL sanitization
function sanitizeURL(url) {
try {
const parsed = new URL(url);
// Only allow http and https
if (!['http:', 'https:'].includes(parsed.protocol)) {
return null;
}
return parsed.toString();
} catch {
return null;
}
}
function sanitizeQueryParam(param) {
return encodeURIComponent(param);
}
// Usage
console.log(sanitizeURL('https://example.com')); // 'https://example.com/'
console.log(sanitizeURL('javascript:alert("XSS")')); // null
console.log(sanitizeQueryParam('hello world')); // 'hello%20world'
XSS Prevention
Content Security Policy
// ✅ Good: CSP headers (server-side)
// Set-Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'
// Client-side: Use textContent instead of innerHTML
function displayUserContent(content) {
const element = document.getElementById('content');
element.textContent = content; // Safe
// NOT: element.innerHTML = content; // Unsafe
}
// Usage
const userInput = '<script>alert("XSS")</script>';
displayUserContent(userInput); // Displays as text, not executed
DOM-based XSS Prevention
// ✅ Good: Prevent DOM-based XSS
function safeSetHTML(element, html) {
// Use DOMPurify library for production
const sanitized = sanitizeHTML(html);
element.innerHTML = sanitized;
}
function safeSetAttribute(element, attr, value) {
// Validate attribute
const allowedAttrs = ['href', 'src', 'alt', 'title'];
if (!allowedAttrs.includes(attr)) {
console.warn(`Attribute ${attr} not allowed`);
return;
}
// Sanitize value
if (attr === 'href' || attr === 'src') {
const sanitized = sanitizeURL(value);
if (sanitized) {
element.setAttribute(attr, sanitized);
}
} else {
element.setAttribute(attr, escapeHTML(value));
}
}
// Usage
const div = document.createElement('div');
safeSetHTML(div, '<p>Hello</p>');
safeSetAttribute(div, 'href', 'https://example.com');
SQL Injection Prevention
Parameterized Queries
// ✅ Good: Use parameterized queries
// With a database library like mysql2/promise
async function getUserByEmail(email) {
// Parameterized query - safe from SQL injection
const query = 'SELECT * FROM users WHERE email = ?';
const [rows] = await connection.execute(query, [email]);
return rows[0];
}
// ❌ Bad: String concatenation - vulnerable
async function getUserByEmailUnsafe(email) {
const query = `SELECT * FROM users WHERE email = '${email}'`;
// Vulnerable to SQL injection!
}
// Usage
const user = await getUserByEmail('[email protected]');
Input Validation for Queries
// ✅ Good: Validate before querying
function validateQueryInput(input) {
// Remove dangerous characters
return input
.replace(/[;'"]/g, '')
.trim()
.substring(0, 100);
}
async function searchUsers(searchTerm) {
const validated = validateQueryInput(searchTerm);
const query = 'SELECT * FROM users WHERE name LIKE ?';
const [rows] = await connection.execute(query, [`%${validated}%`]);
return rows;
}
// Usage
const results = await searchUsers("O'Brien"); // Safe
CSRF Prevention
Token-based CSRF Protection
// ✅ Good: CSRF token validation
class CSRFProtection {
static generateToken() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
static validateToken(token, sessionToken) {
return token === sessionToken;
}
static addTokenToForm(form, token) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'csrf_token';
input.value = token;
form.appendChild(input);
}
}
// Usage
const token = CSRFProtection.generateToken();
const form = document.getElementById('myForm');
CSRFProtection.addTokenToForm(form, token);
SameSite Cookie Attribute
// ✅ Good: Set SameSite cookie attribute (server-side)
// Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
// Client-side: Use fetch with credentials
async function makeRequest(url, data) {
const response = await fetch(url, {
method: 'POST',
credentials: 'include', // Include cookies
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
return response.json();
}
function getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]').content;
}
Practical Security Examples
Secure Form Handler
// ✅ Good: Secure form handler
class SecureFormHandler {
constructor(formId, schema) {
this.form = document.getElementById(formId);
this.schema = schema;
this.setupEventListeners();
}
setupEventListeners() {
this.form.addEventListener('submit', (e) => this.handleSubmit(e));
}
handleSubmit(e) {
e.preventDefault();
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
// Validate
const validation = Validator.validate(data, this.schema);
if (!validation.valid) {
this.displayErrors(validation.errors);
return;
}
// Sanitize
const sanitized = this.sanitizeData(data);
// Submit
this.submitForm(sanitized);
}
sanitizeData(data) {
const sanitized = {};
for (const [key, value] of Object.entries(data)) {
if (typeof value === 'string') {
sanitized[key] = escapeHTML(value.trim());
} else {
sanitized[key] = value;
}
}
return sanitized;
}
displayErrors(errors) {
for (const [field, messages] of Object.entries(errors)) {
const input = this.form.querySelector(`[name="${field}"]`);
if (input) {
input.classList.add('error');
input.title = messages.join(', ');
}
}
}
async submitForm(data) {
try {
const response = await fetch(this.form.action, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});
if (response.ok) {
console.log('Form submitted successfully');
} else {
console.error('Form submission failed');
}
} catch (error) {
console.error('Error submitting form:', error);
}
}
}
// Usage
const schema = {
email: { required: true, email: true },
password: { required: true, minLength: [8] }
};
new SecureFormHandler('loginForm', schema);
Best Practices
- Always validate on server:
// ✅ Good - validate on both client and server // Client: Quick feedback // Server: Security // ❌ Bad - only client validation ```javascript - Use allowlists, not blocklists:
// ✅ Good const allowedTags = ['p', 'br', 'strong', 'em']; // ❌ Bad const blockedTags = ['script', 'iframe']; ```javascript - Escape output:
// ✅ Good element.textContent = userInput; // ❌ Bad element.innerHTML = userInput; ```javascript
Common Mistakes
- Client-side validation only:
// ❌ Bad - can be bypassed if (validateEmail(email)) { submitForm(); } // ✅ Good - validate on server too ```javascript - Using innerHTML with user input:
// ❌ Bad element.innerHTML = userInput; // ✅ Good element.textContent = userInput; ```javascript - Not escaping output:
// ❌ Bad const html = `<p>${userInput}</p>`; // ✅ Good const html = `<p>${escapeHTML(userInput)}</p>`;
Summary
Input validation and sanitization are essential security practices. Key takeaways:
- Validate all input
- Sanitize dangerous content
- Prevent XSS attacks
- Prevent SQL injection
- Prevent CSRF attacks
- Use allowlists
- Escape output
- Validate server-side
Related Resources
- OWASP Input Validation
- XSS Prevention - OWASP
- SQL Injection - OWASP
- CSRF Prevention - OWASP
- DOMPurify Library
Next Steps
- Learn about XSS Prevention and CSP
- Explore CSRF Protection
- Study Secure Coding Practices
- Practice input validation
- Build secure forms
Comments