Forms are how visitors send data to your site: searches, sign-ups, comments, orders. Handling them is where most security problems begin, so this lesson builds the safe habits from the start.
Treat every piece of input as untrusted. It can be missing, the wrong type, far too long, or deliberately malicious, even if your HTML form "only allows" certain values. Anyone can send any request to your server.
GET and POST
| Method | Data travels in | Use for |
|---|---|---|
GET | The URL: search.php?q=arrays | Searches, filters, anything safe to bookmark |
POST | The request body | Anything that changes data: sign-ups, orders, messages |
PHP puts the data in two arrays, $_GET and $_POST. Values are always strings (or arrays for multi-value fields), never ints or bools.
<?php
// search.php?q=php+arrays
$query = trim($_GET['q'] ?? '');
echo 'You searched for: ' . htmlspecialchars($query, ENT_QUOTES, 'UTF-8');
?? '' avoids a warning when the parameter is missing.
Escape output: preventing XSS
Imagine a visitor searches for this:
<script>alert('hacked')</script>
If you echo it directly, the browser runs that script. An attacker can send someone a link that steals their session or rewrites the page. This is cross-site scripting (XSS), one of the most common web vulnerabilities.
htmlspecialchars() turns special characters into harmless entities, so the browser shows them as text:
<?php
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
echo e("<script>alert('hacked')</script>");
// <script>alert('hacked')</script>
Escape at the moment of output, every time you put data into HTML. A tiny helper like e() makes it easy enough that you never skip it. Validation does not replace escaping.
A complete contact form
Save this as contact.php and run it with php -S localhost:8000.
<?php
declare(strict_types=1);
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
$errors = [];
$values = ['name' => '', 'email' => '', 'message' => ''];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. Read and normalise.
$values['name'] = trim((string) ($_POST['name'] ?? ''));
$values['email'] = trim((string) ($_POST['email'] ?? ''));
$values['message'] = trim((string) ($_POST['message'] ?? ''));
// 2. Validate.
if ($values['name'] === '') {
$errors['name'] = 'Enter your name.';
} elseif (mb_strlen($values['name']) > 100) {
$errors['name'] = 'Use 100 characters or fewer.';
}
if (!filter_var($values['email'], FILTER_VALIDATE_EMAIL)) {
$errors['email'] = 'Enter an email address like name@example.com.';
}
if (mb_strlen($values['message']) < 10) {
$errors['message'] = 'Write at least 10 characters.';
}
// 3. Act, then redirect.
if ($errors === []) {
// Save to a database or send an email here.
header('Location: contact.php?sent=1', true, 303);
exit;
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Contact</title></head>
<body>
<?php if (isset($_GET['sent'])): ?>
<p role="status">Thanks, your message was sent.</p>
<?php endif; ?>
<form method="post" novalidate>
<p>
<label for="name">Name</label>
<input id="name" name="name" value="<?= e($values['name']) ?>" required>
<?php if (isset($errors['name'])): ?><span><?= e($errors['name']) ?></span><?php endif; ?>
</p>
<p>
<label for="email">Email</label>
<input id="email" name="email" type="email" value="<?= e($values['email']) ?>" required>
<?php if (isset($errors['email'])): ?><span><?= e($errors['email']) ?></span><?php endif; ?>
</p>
<p>
<label for="message">Message</label>
<textarea id="message" name="message" required><?= e($values['message']) ?></textarea>
<?php if (isset($errors['message'])): ?><span><?= e($errors['message']) ?></span><?php endif; ?>
</p>
<button type="submit">Send</button>
</form>
</body>
</html>
Walk through what it does:
- Normalise:
trim()removes accidental spaces; the(string)cast guards against someone sending an array instead of a string. - Validate on the server: HTML's
requiredandtype="email"help honest users, but anyone can bypass them, so PHP checks again.filter_var(..., FILTER_VALIDATE_EMAIL)checks the email format. - Keep the user's answers: after an error, each field is refilled with the escaped value, so nothing has to be retyped.
- Error messages say how to fix the problem, not just that something is wrong.
- Post/Redirect/Get: after success,
header('Location: ...')with status 303 sends the browser to a normal GET page. Refreshing it will not resubmit the form.
header() must be called before any output, including blank lines or spaces before <?php. Otherwise PHP reports "headers already sent".
What this example does not cover yet
Real forms also need:
- CSRF protection: a hidden secret token that proves the form came from your own site.
- Prepared statements when saving to a database, to prevent SQL injection.
- Rate limiting or a CAPTCHA to slow down spam.
Frameworks and WordPress provide these for you; the PHP and MySQL course covers prepared statements in depth.
Summary
$_GETand$_POSThold input as strings; read them with?? ''and normalise withtrim().- Never trust input: validate on the server even if the HTML form validates too.
- Escape every value you output into HTML with
htmlspecialchars($value, ENT_QUOTES, 'UTF-8'). - Redisplay the form with the user's values and clear error messages.
- After a successful POST, redirect with status 303.