Lesson 3 of 7 · 30 min

Input Validation and Output Encoding: Preventing Injection and XSS

Validate data at the boundary, use parameterised queries and encode output for the context where it appears.

In this lesson1. Trust boundaries2. Validating input3. Parameterised queries4. Output encoding and cross-site scripting

Trust boundaries

A trust boundary is any point where data enters your system from somewhere you do not control: form fields, query strings, JSON bodies, headers, cookies, file uploads, webhooks from partners and even data from your own database if users wrote it. Treat everything crossing a boundary as untrusted until it has been checked. This includes data from your own mobile app, because anyone can send requests to your API without using the app.

Two complementary defences handle untrusted data. Input validation checks that data has the expected shape before you use it. Output encoding makes sure data is treated as plain data, not code, when you place it into HTML, SQL, a shell command or another interpreter. You need both. Validation reduces what can go wrong, and encoding prevents what remains from being executed.

Validating input

Validate on the server using an allowlist approach: define what is acceptable and reject everything else. Check type, length, format and range. An Indian PIN code should be six digits, a quantity should be a positive integer within a sensible range and a status should be one of a fixed set of values. Use a schema validation library, such as zod for TypeScript or Pydantic for Python, so rules are declared once and applied consistently.

Client-side validation is useful for user experience but provides no security, because attackers bypass the browser entirely. Validate file uploads by checking size and actual content type, store them outside the web root with generated names and never execute them. Reject unexpected fields in JSON bodies so clients cannot set values such as a role or price that they should not control, a problem known as mass assignment.

Parameterised queries

SQL injection happens when user input is joined into a query string. The reliable fix is to use parameterised queries, also called prepared statements, where the query structure and the values travel separately to the database. The database never interprets the values as SQL. Most ORMs and query builders do this for you, but raw query features in those tools can still be misused, so review any place where strings are concatenated into queries. The example is illustrative; check each library's documentation.

typescript · illustrative
// Illustrative TypeScript using zod and node-postgres (pg).
// See zod.dev and node-postgres.com for current APIs.
import { z } from 'zod';
import { Pool } from 'pg';

const pool = new Pool();

const OrderQuery = z.object({
  orderId: z.string().uuid(),
  status: z.enum(['pending', 'paid', 'shipped']).optional(),
});

export async function findOrder(input: unknown, userId: string) {
  const q = OrderQuery.parse(input); // throws on invalid input

  // Parameterised query: values are never pasted into the SQL string
  const result = await pool.query(
    'SELECT id, status, total FROM orders WHERE id = $1 AND user_id = $2',
    [q.orderId, userId],
  );
  return result.rows[0] ?? null;
}

Output encoding and cross-site scripting

Cross-site scripting happens when untrusted data is placed into a page in a way that lets it run as script. The defence is encoding output for its context: HTML body text, HTML attributes, JavaScript, CSS and URLs each need different encoding. Modern frameworks such as React, Angular and Vue, and template engines such as Jinja, encode text by default, which prevents most cases. The danger comes from features that bypass this, such as inserting raw HTML.

If you must render user-supplied HTML, for example rich text in a support ticket, sanitise it with a maintained library such as DOMPurify using a strict allowlist of tags and attributes. Never build HTML strings by hand from user input. Add a Content Security Policy, covered in lesson five, as a second layer. XDQ Labs code reviews flag every use of raw HTML insertion so a second person confirms the data is sanitised.

  • Prefer framework defaults that encode output automatically
  • Search the codebase for raw HTML insertion and review each use
  • Sanitise rich text with a maintained library and an allowlist
  • Never pass user input to shell commands or eval
Key takeaways
  • Treat all data crossing a trust boundary as untrusted
  • Validate on the server with allowlists and schema libraries
  • Use parameterised queries for every database call
  • Encode output for its context and rely on framework defaults
  • Sanitise any user-supplied HTML with a maintained library
Try it

Search your codebase for queries built with string concatenation or template strings and convert any you find to parameterised queries. Add schema validation to one API endpoint, rejecting unknown fields. Search for raw HTML insertion in your frontend and confirm each use is sanitised or removed.

Check your understanding

1. Why is client-side validation not enough for security?

2. What reliably prevents SQL injection?

3. When is cross-site scripting most likely in a modern framework?

← PreviousSecure Authentication and Session Management Done RightNext lesson →Secrets Management for Developers and Small Teams
XDQ xAgents · AI lab for your company
While you sleep, your AI agents keep working.

From USD 49 or INR 2,999 a month. A person approves every step.

Start free, 14 days →See every agent300 credits · no card needed · prices include taxes🚀 Startup? Free landing page for 30 days →Need custom software or a security review? Send a brief →