User Input Sanitization in JavaScript: Practical and Safe Patterns
User input is untrusted by default. Whether it comes from a form, URL parameters, or an API request, it must be validated and sanitized before you use it. This post explains safe, practical patterns for both frontend and backend scenarios using TypeScript examples and expected output.
Quick start
function sanitizeText(input: string): string {
return input.trim().replace(/\s+/g, " ");
}
console.log(sanitizeText(" hello world "));
Result:
hello world
Sanitization checklist
- Validate first: check type, length, and allowed characters.
- Normalize input: trim and collapse whitespace.
- Escape on output: especially when rendering to HTML.
- Prefer allow-lists over block-lists.
- Keep raw input only if you must (for audits or debugging).
Validation vs sanitization vs encoding
These are related but different:
- Validation answers: "Is this input acceptable?"
- Sanitization answers: "Can I normalize this input to a safe version?"
- Encoding/Escaping answers: "How do I safely output this input in a specific context?"
Most real systems need all three.
Frontend: render text safely
In the browser, the safest default is to avoid HTML injection entirely and render text using textContent.
const userInput = "<b>Hi</b>";
const safe = document.createElement("span");
safe.textContent = userInput;
console.log(safe.textContent);
Result:
<b>Hi</b>
The string is treated as text, not HTML.
When you must output HTML, escape it
If you absolutely must output HTML from user input, escape it first. This prevents scripts or tags from being interpreted by the browser.
function escapeHtml(input: string): string {
return input
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const unsafe = `<img src=x onerror="alert(1)">`;
console.log(escapeHtml(unsafe));
Result:
<img src=x onerror="alert(1)">
Context-aware encoding
Always encode based on the output context. HTML text, HTML attributes, URLs, and JSON each have different rules.
function escapeAttribute(input: string): string {
return escapeHtml(input).replace(/`/g, "`");
}
const attribute = escapeAttribute(`hello "world"`);
console.log(attribute);
Result:
hello "world"
Validate with allow-lists
Allow-lists are safer than trying to block every possible bad input. For usernames, keep it simple.
function isValidUsername(input: string): boolean {
return /^[a-z0-9_]{3,20}$/i.test(input);
}
console.log(isValidUsername("luca_nerlich"));
Result:
true
Normalize before validating
Normalize whitespace and casing to make validation rules stable.
function normalizeEmail(input: string): string {
return input.trim().toLowerCase();
}
console.log(normalizeEmail(" USER@EXAMPLE.COM "));
Result:
user@example.com
Numeric input normalization
Convert numeric input explicitly and reject invalid values early.
function toPositiveInt(input: string): number | null {
const value = Number(input);
if (!Number.isInteger(value) || value <= 0) return null;
return value;
}
console.log(toPositiveInt("42"));
Result:
42
Backend basics: validate request data
In backend code, validation is non-negotiable. Convert types explicitly and reject invalid data early.
type CreateUserInput = { name: string; age: number };
function validateCreateUser(input: unknown): CreateUserInput | null {
if (typeof input !== "object" || input === null) return null;
const value = input as Record<string, unknown>;
if (typeof value.name !== "string") return null;
if (typeof value.age !== "number" || !Number.isFinite(value.age)) return null;
return { name: value.name, age: value.age };
}
const candidate = { name: "Ada", age: 36 };
console.log(validateCreateUser(candidate));
Result:
{ name: "Ada", age: 36 }
Encode for URLs
When user input ends up in URLs, use encodeURIComponent to prevent broken URLs or injection.
const query = "cats & dogs";
const url = `https://example.com/search?q=${encodeURIComponent(query)}`;
console.log(url);
Result:
https://example.com/search?q=cats%20%26%20dogs
Avoid unsafe sinks
Some APIs interpret strings as code or HTML. Avoid passing user input into these sinks. Prefer safe APIs such as textContent, parameterized queries, and built-in templating that escapes by default.
Handling rich text safely
If you allow rich text, use a well-maintained sanitizer on the backend to allow only a small subset of tags and attributes. Keep the allow-list tight and explicitly defined. Avoid writing a custom HTML parser unless you have a strong reason.
Common pitfalls
- Trusting client-side validation: users can bypass it.
- Using block-lists: they miss new attack patterns.
- Mixing validation and sanitization: they solve different problems.
- Rendering with innerHTML: avoid it for user input.
- Logging raw input: can leak sensitive data.
Best practices
- Validate on every boundary (client and server).
- Use allow-lists for IDs, usernames, and tags.
- Escape output based on context (HTML, URL, SQL, JSON).
- Keep validation rules centralized to avoid drift.
- Test with malicious-looking inputs.
FAQ: input sanitization in JavaScript
What is the difference between validation and sanitization?
Validation checks that input matches your expected format; sanitization normalizes or escapes it to make it safe.
const raw = " hello ";
const sanitized = raw.trim();
console.log(sanitized);
Result:
hello
How do I prevent XSS in JavaScript?
Avoid inserting raw user input into HTML. Use textContent or escape the content before rendering.
const input = "<script>alert(1)</script>";
const safeText = document.createTextNode(input).textContent;
console.log(safeText);
Result:
<script>alert(1)</script>
Should I sanitize on the backend if I already do it on the frontend?
Yes. Frontend validation improves UX, but backend validation is the real security boundary.
const isValid = isValidUsername("user_01");
console.log(isValid);
Result:
true
Summary
Sanitization and validation are essential for secure JavaScript. Validate inputs with allow-lists, normalize consistently, escape output by context, and never rely on the frontend alone.