Security Best Practices
Review the core security practices every PHP application should follow, from preventing SQL injection and XSS to protecting against CSRF.
Introduction
A PHP application that works correctly is not the same as a PHP application that is safe to put on the internet. Because PHP so often handles logins, payments, and user-submitted content, security has to be a habit, not an afterthought bolted on at the end.
Several of these topics — prepared statements and password hashing in particular — were covered in earlier lessons when you built database and authentication features. This lesson pulls those ideas together with a few new ones, forming a single security checklist you can apply to every project you build.
- How prepared statements prevent SQL injection.
- How to prevent XSS by escaping output correctly.
- Why passwords must always be hashed, never stored in plain text.
- The basics of CSRF protection using tokens.
- Why validating input and keeping software updated matter just as much as any single technique.
Preventing SQL Injection
SQL injection happens when untrusted input is concatenated directly into a SQL query, letting an attacker change what the query actually does. You saw this danger back in the Prepared Statements lesson — the fix is to never build queries with string concatenation.
$id = $_GET['id'];
$result = $conn->query("SELECT * FROM users WHERE id = $id");
// An attacker could pass ?id=1 OR 1=1 and expose every row$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $_GET['id']);
$stmt->execute();
$result = $stmt->get_result();A prepared statement sends the query structure and the user data separately, so the database never confuses user input for part of the SQL command itself.
Preventing XSS
Cross-Site Scripting (XSS) happens when untrusted input is output into HTML without escaping, allowing an attacker to inject a <script> tag or other markup that runs in another user's browser. The fix is simple and consistent: always escape output with htmlspecialchars().
$comment = $_POST['comment'];
echo "<p>$comment</p>";
// An attacker could submit <script>stealCookies()</script>$comment = $_POST['comment'];
echo "<p>" . htmlspecialchars($comment, ENT_QUOTES, 'UTF-8') . "</p>";Any time a variable containing user input is printed into HTML, wrap it in htmlspecialchars(). It is far safer to escape too often than to forget once.
Password Hashing Recap
As covered in the Password Hashing lesson, passwords must never be stored as plain text or with a fast, general-purpose hash like md5() or sha1(). PHP's built-in password functions handle this correctly and safely by default.
// When a user registers:
$hash = password_hash($plainTextPassword, PASSWORD_DEFAULT);
// Store $hash in the database, never the original password
// When a user logs in:
if (password_verify($enteredPassword, $hash)) {
echo "Login successful.";
} else {
echo "Invalid credentials.";
}Login successful.CSRF Protection Basics
Cross-Site Request Forgery (CSRF) tricks a logged-in user's browser into submitting a request they never intended — for example, a malicious site silently submitting a form that transfers money on a banking site where the user is already logged in. The standard defense is a CSRF token: a random, secret value embedded in every form and verified on submission.
<?php
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<form method="POST" action="process.php">
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
<input type="text" name="message">
<button type="submit">Submit</button>
</form><?php
session_start();
$submittedToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $submittedToken)) {
http_response_code(403);
die("Invalid CSRF token.");
}
echo "Form processed safely.";Form processed safely.Because the token is stored in the user's session and only known to the legitimate form, a request forged from another site cannot supply a matching token.
Validating and Sanitizing Input
SQL injection, XSS, and CSRF are all specific consequences of a more general rule: never trust data that comes from the user. Every piece of incoming data — form fields, query strings, uploaded files, even cookies — should be validated against what you expect before it is used.
$email = trim($_POST['email'] ?? '');
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
die("Please provide a valid email address.");
}
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 120],
]);
if ($age === false) {
die("Please provide a valid age.");
}
echo "Input validated successfully.";Input validated successfully.Keeping PHP and Dependencies Updated
Security is not only about the code you write — the PHP version and Composer packages you depend on matter too. Older PHP versions and outdated libraries can carry known, publicly documented vulnerabilities that attackers actively scan for.
Upgrade PHP Regularly
Run a currently supported PHP version so you continue receiving security patches.
Update Composer Packages
Regularly run composer update and review changelogs for security fixes in your dependencies.
Watch for Advisories
Tools like composer audit can flag dependencies with known published vulnerabilities.
Security Checklist
- Use prepared statements for every database query that includes user input.
- Escape every piece of user-controlled output with htmlspecialchars().
- Hash passwords with password_hash() and verify with password_verify() — never store plain text.
- Protect every state-changing form with a CSRF token checked using hash_equals().
- Validate and sanitize all incoming input, using PHP's filter_var() where appropriate.
- Keep your PHP version and Composer dependencies current.
Common Mistakes
- Concatenating user input directly into SQL strings instead of using prepared statements.
- Printing user input into HTML without htmlspecialchars(), even "just for now" during development.
- Comparing CSRF tokens with == instead of the timing-safe hash_equals().
- Assuming client-side (JavaScript) validation is enough — it must always be re-checked on the server.
- Trusting that a framework or library handles security automatically without understanding what it is actually doing.
Best Practices
- Treat every value coming from $_GET, $_POST, $_COOKIE, and file uploads as untrusted by default.
- Apply security consistently across the whole application, not only on forms that "seem" sensitive.
- Keep a written checklist (like the one above) and run through it before launching any project.
- Follow security-focused PHP news and changelogs so you know when to upgrade.
Frequently Asked Questions
Is it enough to just use a framework like Laravel to be secure?
Frameworks provide strong built-in protections (like automatic CSRF tokens and query building), but you still need to understand these concepts, since misusing a framework's features can reintroduce the exact same vulnerabilities.
What is the single most important security practice in this lesson?
There is no single most important one — SQL injection, XSS, weak password storage, and CSRF are all common, serious, and independent risks. All of them need to be addressed.
Do I need CSRF tokens on every form?
Any form that changes state (creating, updating, or deleting data) should be protected. Simple read-only GET requests, like a search box, typically do not need one.
Why use hash_equals() instead of a normal == comparison for tokens?
A normal string comparison can leak timing information that theoretically helps an attacker guess a secret value byte by byte. hash_equals() compares in constant time, avoiding that risk.
Is validating input on the client side with JavaScript pointless then?
Client-side validation is still useful for a fast, friendly user experience, but it can always be bypassed, so the server must independently validate everything again.
Key Takeaways
- Prepared statements prevent SQL injection by separating query structure from user data.
- htmlspecialchars() prevents XSS by escaping user input before it is printed as HTML.
- Passwords must always be hashed with password_hash() and checked with password_verify().
- CSRF tokens, verified with hash_equals(), protect state-changing forms from forged requests.
- All user input must be validated and sanitized, and PHP plus dependencies kept up to date.
Summary
Security in PHP is less about any single clever trick and more about consistently applying a small set of well-known practices: prepared statements, output escaping, proper password hashing, CSRF tokens, input validation, and staying up to date. Together, these form a solid baseline for any real-world application.
In this lesson, you reviewed and consolidated the core PHP security practices covered throughout the course. Next, you will look at performance optimization techniques to keep your applications fast as they grow.
- You can explain how prepared statements prevent SQL injection.
- You know to escape all user-facing output with htmlspecialchars().
- You understand why passwords must be hashed, never stored as plain text.
- You can implement basic CSRF protection using a token and hash_equals().
- You have a security checklist you can apply to every project.