Skip to main content
โšก Calmops

Input Validation and Sanitization in JavaScript

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 = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#039;'
  };
  return text.replace(/[&<>"']/g, (char) => map[char]);
}

// Usage
const userInput = '<script>alert("XSS")</script>';
console.log(sanitizeHTML(userInput));
// &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

console.log(escapeHTML(userInput));
// &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

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);
// โœ… 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

  1. Always validate on server:

    // โœ… Good - validate on both client and server
    // Client: Quick feedback
    // Server: Security
    
    // โŒ Bad - only client validation
    
  2. Use allowlists, not blocklists:

    // โœ… Good
    const allowedTags = ['p', 'br', 'strong', 'em'];
    
    // โŒ Bad
    const blockedTags = ['script', 'iframe'];
    
  3. Escape output:

    // โœ… Good
    element.textContent = userInput;
    
    // โŒ Bad
    element.innerHTML = userInput;
    

Common Mistakes

  1. Client-side validation only:

    // โŒ Bad - can be bypassed
    if (validateEmail(email)) {
      submitForm();
    }
    
    // โœ… Good - validate on server too
    
  2. Using innerHTML with user input:

    // โŒ Bad
    element.innerHTML = userInput;
    
    // โœ… Good
    element.textContent = userInput;
    
  3. 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

Next Steps

Comments