Input Sanitization and Validation in JavaScript

· 7 min read · Updated March 30, 2026 · intermediate
security javascript validation xss

Every application that accepts user input is a potential attack surface. Whether it’s a comment form, a search box, or an API endpoint, your code needs to distinguish between legitimate data and malicious payloads. Input validation and sanitization are the two layers that make that distinction possible.

Validation vs Sanitization

These two terms get used interchangeably, but they solve different problems.

Input validation checks whether data matches expected format, type, or range. It sits at the gate — rejecting what doesn’t fit your criteria before your code even processes it. Validation is strict by nature: if the data doesn’t pass the rules, it’s out.

Input sanitization transforms potentially dangerous input into something safe. Rather than rejecting bad data outright, sanitization strips or neutralizes dangerous characters so the input can still be used without causing harm.

You need both. Validation alone leaves gaps — a correctly formatted string can still contain malicious content. Sanitization alone is incomplete — you might let garbage through that breaks your application. Together they form a defense-in-depth strategy.

// Validation: reject invalid email format
const email = req.body.email;
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
  return res.status(400).send('Invalid email');
}

// Sanitization: normalize the valid email
const safeEmail = email.toLowerCase().trim();

Common Attack Vectors

Understanding what you’re defending against shapes how you build the defenses.

Cross-Site Scripting (XSS)

XSS happens when user input finds its way into a web page without proper escaping, allowing attacker-controlled scripts to execute in other users’ browsers.

Three main varieties:

  • Reflected XSS: user input echoed in a server response without encoding, like search results
  • Stored XSS: malicious input saved to a database and served to other users
  • DOM-based XSS: client-side JavaScript reads input and writes it to the DOM unsafely
// Vulnerable: innerHTML lets scripts execute
element.innerHTML = `<p>${userInput}</p>`;

// Safe: textContent auto-escapes
element.textContent = userInput;

SQL Injection

When user input reaches a database query through string concatenation, attackers can break out of the intended query structure. This applies in Node.js environments that interact with databases.

// Dangerous: string concatenation allows injection
db.query(`SELECT * FROM users WHERE name = '${name}'`);
// Input: "'; DROP TABLE users; --" becomes catastrophic

// Safe: parameterized query separates code from data
db.query('SELECT * FROM users WHERE name = $1', [name]);

Command Injection

Passing unsanitized user input to shell execution functions like child_process.exec, eval, or the Function constructor can let attackers run arbitrary system commands.

// Never do this
eval(`console.log("${userInput}")`);

// Never this either
child_process.exec(`echo ${userInput}`);

Browser-Side Defenses

Using textContent Instead of innerHTML

The simplest XSS defense in browser JavaScript is choosing the right DOM property. textContent treats its value as plain text. innerHTML parses it as HTML, which means any embedded <script> tags execute.

If you must use innerHTML, sanitize the input first.

DOMPurify

For applications that need to accept structured HTML (think rich text editors), DOMPurify is the standard browser-side sanitizer. It parses HTML and strips anything dangerous while preserving safe markup.

import DOMPurify from 'dompurify';

const dirty = '<script>alert("xss")</script><p>Hello <b>world</b></p>';
const clean = DOMPurify.sanitize(dirty);
// clean === '<p>Hello <b>world</b></p>'

// Lock down to a minimal set of tags and attributes
const strictClean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'p', 'em', 'strong'],
  ALLOWED_ATTR: ['href']
});

DOMPurify operates on the actual DOM, not on strings, which protects against mutation-based bypasses that trick string-based sanitizers.

Content Security Policy

CSP is an HTTP response header that tells the browser what resources it is allowed to load and from where. It is not a substitute for input validation, but it limits what injected scripts can do even if XSS slips through.

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

Breaking it down:

  • default-src 'self' restricts all resources to your own origin by default
  • script-src 'self' allows scripts only from your own origin
  • object-src 'none' blocks Flash, Java, and other plugin content entirely

For inline scripts, use a nonce — a random token the server generates per request:

Content-Security-Policy: script-src 'self' 'nonce-abc123'
// Server generates a unique nonce per request
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('hex');
  next();
});

app.get('/', (req, res) => {
  res.render('index', {
    nonce: res.locals.nonce,
    // Pass nonce to the template to add to <script nonce="...">
  });
});

Context-Aware Escaping

Escaping rules change depending on where you’re inserting data. The same character requires different handling in HTML, JavaScript strings, URLs, and CSS.

ContextCharacters to EscapeMethod
HTML body<, >, &, ", 'textContent or innerText
HTML attribute", ', <, >Always quote attributes
JavaScript string\, ', ", `JSON encode or escape manually
URL parameterSpecial charsencodeURIComponent()

The browser’s native textContent handles HTML context automatically. For other contexts, encodeURIComponent() is the standard choice for URL parameters.

Server-Side Validation in Node.js

Client-side validation improves UX but means nothing to an attacker who can call your API directly. Every endpoint must validate on the server.

Zod

Zod has become a go-to choice for Node.js validation because it combines schema definition with TypeScript type inference. You define the shape once and get both runtime validation and compile-time types.

import { z } from 'zod';

const UserSchema = z.object({
  username: z.string()
    .min(3)
    .max(30)
    .regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  age: z.number().int().positive().optional()
});

const result = UserSchema.safeParse(req.body);

if (!result.success) {
  return res.status(400).json({
    errors: result.error.issues
  });
}

// result.data is typed as { username: string; email: string; age?: number }
const user = result.data;

safeParse returns an object with either data (on success) or error (on failure) — it never throws. This makes error handling straightforward.

express-validator

For Express middleware chains, express-validator offers a fluent, chainable API:

import { body, validationResult } from 'express-validator';

app.post('/register', [
  body('email')
    .isEmail()
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 })
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
  body('age')
    .optional()
    .isInt({ min: 0, max: 150 })
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
  }
  // proceed
});

Building a Complete Validation Pipeline

A realistic form handler combines schema validation with HTML sanitization and safe database writes.

import DOMPurify from 'dompurify';
import { z } from 'zod';

const CommentSchema = z.object({
  username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
  email: z.string().email(),
  content: z.string().min(1).max(1000)
});

app.post('/comments', async (req, res) => {
  // 1. Validate the raw shape with Zod
  const parsed = CommentSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ error: parsed.error.flatten() });
  }

  // 2. Sanitize HTML content with DOMPurify
  const sanitizedContent = DOMPurify.sanitize(parsed.data.content, {
    ALLOWED_TAGS: ['p', 'b', 'i', 'em', 'strong', 'a'],
    ALLOWED_ATTR: ['href']
  });

  // 3. Write using parameterized query
  await db.query(
    'INSERT INTO comments (username, email, content) VALUES ($1, $2, $3)',
    [parsed.data.username, parsed.data.email, sanitizedContent]
  );

  res.status(201).json({ message: 'Comment saved' });
});

This flow does three things in order: it validates the structure and types with Zod, sanitizes any HTML markup with DOMPurify, and inserts using a parameterized query so database input cannot break out of the query structure.

Key Principles

Whitelist over blacklist. Trying to block known dangerous patterns is a losing game — new bypasses appear constantly. Define exactly what is allowed and reject everything else.

// Blacklist approach — easy to bypass
const bad = input.replace(/<script/gi, '');

// Whitelist approach — strict and predictable
const safe = input.replace(/[^a-zA-Z0-9 .,!?-]/g, '');

Never trust client-side validation. Browser JavaScript is fully inspectable and modifiable. Use it to improve form UX, but always re-validate on the server.

Sanitize late, validate early. Validate as soon as data enters your system. Sanitize right before the data leaves — when you know exactly where it’s going.

Conclusion

Input validation and sanitization are complementary defenses. Validation rejects bad data at the boundary using strict rules. Sanitization neutralizes dangerous characters so input can still be used safely. Together they protect against XSS, SQL injection, and command injection — the three most common attack vectors targeting user input.

The practical stack is straightforward: Zod or express-validator for server-side schema validation, DOMPurify for HTML sanitization in the browser, parameterized queries for database writes, and CSP as a safety net that limits the damage any successful injection can cause.

Start with validation — every endpoint should know what shape of data it expects and reject anything that doesn’t match. Add sanitization for any context where HTML or rich content is involved. Set a strict CSP so that even if something slips through, the browser blocks the damage.

See Also


Written

  • File: sites/jsguides/src/content/tutorials/security-input-sanitization.md
  • Words: ~1150
  • Read time: 10 min
  • Topics covered: validation vs sanitization, XSS, SQL injection, command injection, DOMPurify, CSP, Zod, express-validator, whitelist vs blacklist, parameterized queries, context-aware escaping
  • Verified via: MDN DOMPurify docs, Zod docs, express-validator docs, Mozilla CSP documentation
  • Unverified items: none