Input Validation and Sanitization in JavaScript
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 -
Use allowlists, not blocklists:
// โ Good const allowedTags = ['p', 'br', 'strong', 'em']; // โ Bad const blockedTags = ['script', 'iframe']; -
Escape output:
// โ Good element.textContent = userInput; // โ Bad element.innerHTML = userInput;
Common Mistakes
-
Client-side validation only:
// โ Bad - can be bypassed if (validateEmail(email)) { submitForm(); } // โ Good - validate on server too -
Using innerHTML with user input:
// โ Bad element.innerHTML = userInput; // โ Good element.textContent = userInput; -
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