Phase 2 · Web FundamentalsModule 10~48 min read

Validation, Sanitization & Escaping

Treat browser input as untrusted and apply the right validation, normalization, and output escaping at each boundary.

What you'll learn

Secure input handling is a pipeline, not one magic function. You'll distinguish validation, normalization, conversion, and context-specific escaping—and design errors people can actually fix.

  • Identify external trust boundaries
  • Collect values without type assumptions
  • Validate syntax and business rules
  • Represent errors by field
  • Escape data for HTML text and attribute contexts
  • Carry only validated values into application logic

Everything outside the process is untrusted

Form fields, query strings, headers, cookies, uploaded files, API responses, database records, and environment configuration may be absent, malformed, stale, or hostile. Trust begins only after your application proves a value meets a specific contract.

Sanitization is not validation

Changing input does not prove it is acceptable. Silently deleting characters from an invalid email, price, or identifier can create a different value with unintended meaning.
OperationQuestion it answersExample
NormalizeCan equivalent representations be made consistent?Trim surrounding spaces from a display name
ValidateDoes this value satisfy the contract?Age is an integer from 13 to 120
ConvertWhat runtime type should represent it?Validated digit text becomes an integer
EscapeHow can this data be safely inserted here?Encode special characters in HTML text

Use a repeatable input pipeline

From raw input to safe output

STEP 1

Collect

Read optional raw values without assumptions.

STEP 2

Normalize

Trim or canonicalize only when the domain allows it.

STEP 3

Validate

Prove values satisfy syntax and business rules.

STEP 4

Convert

Create the types the application actually needs.

STEP 5

Use

Pass trusted values into the domain operation.

STEP 6

Escape

Encode data for its final output context.

Key idea

Validate for the domain; escape for the destination. A valid biography may contain characters that must still be escaped in HTML, JavaScript, a URL, or a shell command.

Validate field types and rules

validate-profile.php
<?php

declare(strict_types=1);

$raw = [
    'name' => $_POST['name'] ?? null,
    'email' => $_POST['email'] ?? null,
    'age' => $_POST['age'] ?? null,
];

$name = is_string($raw['name']) ? trim($raw['name']) : '';
$email = is_string($raw['email']) ? trim($raw['email']) : '';
$age = filter_var($raw['age'], FILTER_VALIDATE_INT, [
    'options' => ['min_range' => 13, 'max_range' => 120],
]);

$errors = [];

if ($name === '' || mb_strlen($name) > 80) {
    $errors['name'] = 'Enter a name between 1 and 80 characters.';
}

if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
    $errors['email'] = 'Enter a valid email address.';
}

if ($age === false) {
    $errors['age'] = 'Age must be a whole number from 13 to 120.';
}

Notice the order: confirm a raw field is a string before trimming it; validate age before treating it as an integer; store errors by the same keys used by the form.

InputUseful starting point
EmailFILTER_VALIDATE_EMAIL
Integer rangeFILTER_VALIDATE_INT + min_range/max_range
URLFILTER_VALIDATE_URL plus an allowed-scheme rule
Enum-like choicein_array($value, $allowed, true)
DateParse strictly, then verify the parsed components
Free textType, required/optional rule, and a domain length limit

Note

A syntactically valid email does not prove the mailbox belongs to a person. Validation establishes only the contract you actually checked; ownership requires a verification flow.

Design errors around recovery

A useful error identifies the field and tells the user how to fix it without exposing internal implementation. Keep field errors separate from a form-level error such as “We could not save your changes.”

error-helper.php
<?php

function errorFor(array $errors, string $field): ?string
{
    $message = $errors[$field] ?? null;
    return is_string($message) ? $message : null;
}

$emailError = errorFor($errors, 'email');
?>

<?php if ($emailError !== null): ?>
  <p id="email-error" role="alert">
    <?= htmlspecialchars($emailError, ENT_QUOTES, 'UTF-8') ?>
  </p>
<?php endif; ?>

Escape at the last responsible moment

For HTML text and quoted attributes, use htmlspecialchars() with quotes enabled and an explicit encoding. Keep raw validated values internally; escaping too early causes double-encoding and couples domain data to one output format.

escape.php
<?php

function e(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

$displayName = '<strong>Maya</strong>';
?>

<p>Welcome, <?= e($displayName) ?></p>
<input value="<?= e($displayName) ?>">

Context changes the encoder

HTML escaping is not the right encoding for a JavaScript string, CSS value, URL component, HTTP header, or SQL query. Prefer APIs that keep data separate from code, such as JSON encoders and PDO parameters.

Complete registration-validation boundary

register.php
<?php

declare(strict_types=1);

$errors = [];
$email = '';

function e(?string $value): string
{
    return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST') {
    $email = is_string($_POST['email'] ?? null)
        ? trim($_POST['email'])
        : '';

    if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) {
        $errors['email'] = 'Enter a valid email address.';
    }

    if ($errors === []) {
        // Pass $email to the registration service.
        header('Location: /welcome', true, 303);
        exit;
    }
}
?>

<form method="post" novalidate>
  <label for="email">Email</label>
  <input id="email" name="email" type="email"
         value="<?= e($email) ?>"
         aria-describedby="email-error">
  <?php if (isset($errors['email'])): ?>
    <p id="email-error" role="alert"><?= e($errors['email']) ?></p>
  <?php endif; ?>
  <button>Create account</button>
</form>

Tip

Using novalidate during development makes it easy to exercise server-side errors. In the finished interface, browser validation can provide faster feedback while the server remains authoritative.

Recap & quick check

Key takeaways

  • External data stays untrusted until it satisfies a specific contract.
  • Collect, normalize, validate, convert, use, then escape for the output context.
  • Sanitization changes data; validation accepts or rejects it.
  • Field-keyed errors make accessible redisplay and testing simpler.
  • Keep validated internal data raw and escape only when inserting it into a destination.

Quick check

1. What does validation prove?

2. When should HTML escaping normally occur?

3. Why validate before casting?

4. Which structure best supports field-level errors?