PHP Security Basics
The most common PHP vulnerabilities and the built-in tools that prevent them.
3 min read
Because PHP directly handles form input, database queries, and rendered HTML, a handful of well-known vulnerability classes show up constantly in PHP code that wasn't written defensively. Every one of them has a straightforward fix.
SQL injection
<?php
// Vulnerable: user input concatenated directly into SQL
$username = $_GET['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
// An attacker submitting `' OR '1'='1` rewrites the query's logic entirely.
// Safe: a prepared statement with a bound parameter
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ?");
$stmt->execute([$username]);A prepared statement sends the query structure and the user's data to the database separately — the database never interprets the data as part of the SQL syntax, no matter what characters it contains. Never build a SQL query by concatenating user input into a string; PDO (PHP's database abstraction layer) and every framework's ORM use prepared statements by default.
Cross-site scripting (XSS)
<?php
// Vulnerable: raw user input echoed directly into HTML
echo "Welcome, " . $_GET['name'];
// ?name=<script>stealCookies()</script> would execute in every visitor's browser
// Safe: escape before outputting
echo "Welcome, " . htmlspecialchars($_GET['name'], ENT_QUOTES, 'UTF-8');htmlspecialchars() converts characters like <, >, and " into their HTML entity equivalents, so injected markup renders as inert text instead of executing as real HTML or JavaScript. Any value that came from user input — a form field, a URL parameter, even data read back from your own database if it originated from user input — needs escaping before it's placed into an HTML response.
Password hashing
<?php
// Storing a password
$hash = password_hash($plainPassword, PASSWORD_DEFAULT);
// Verifying a login attempt
if (password_verify($submittedPassword, $hash)) {
echo "Login successful";
}Never store a password in plain text, and never hash it yourself with a general-purpose function like md5() or sha1() — both are far too fast, making them practical to brute-force. password_hash() uses a strong, purpose-built algorithm (bcrypt by default) with a random salt built in, and PASSWORD_DEFAULT automatically tracks the current best-practice algorithm as PHP itself is updated.
Cross-site request forgery (CSRF)
<form action="/transfer-funds" method="post">
<input type="hidden" name="csrf_token" value="<?= $csrfToken ?>" />
<input type="number" name="amount" />
<button type="submit">Transfer</button>
</form><?php
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die("Invalid request.");
}CSRF tricks a logged-in user's browser into submitting a request they never intended (e.g. a malicious page silently submitting a form to your site, riding on the victim's existing session cookie). A unique, unpredictable token generated per session (or per form) and verified on submission — as shown above — proves the request actually came from your own form, not a forged one elsewhere. Every major framework generates and checks this automatically.
File upload validation
<?php
$allowedTypes = ['image/jpeg', 'image/png'];
$fileType = mime_content_type($_FILES['avatar']['tmp_name']);
if (!in_array($fileType, $allowedTypes, true)) {
die("Invalid file type.");
}Never trust a file's claimed extension or its client-supplied MIME type from $_FILES['avatar']['type'] — both can be spoofed. Check the file's actual content type with mime_content_type(), restrict allowed types explicitly, and store uploads outside the publicly served web root (or under a randomly generated filename) so an uploaded file can never be directly executed as a script.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.