Each snippet below has a real vulnerability. Find it, explain it, then fix it. Try before you open the solution.
Exercise 1: the login query
<?php
$email = $_POST["email"];
$password = $_POST["password"];
$sql = "SELECT * FROM users WHERE email = '$email' AND password = '" . md5($password) . "'";
$user = $pdo->query($sql)->fetch();
What are the two problems?
Show the answer
- SQL injection:
$emailis joined into the SQL. - Weak password storage:
md5()is a fast, unsalted hash.
A safer version:
<?php
$pdo = require __DIR__ . "/db.php";
$pdo->prepare("UPDATE users SET password_hash = ? WHERE email = 'ana@example.com'")
->execute([password_hash("a long passphrase", PASSWORD_DEFAULT)]);
$_POST = ["email" => "ana@example.com", "password" => "a long passphrase"];
$statement = $pdo->prepare("SELECT id, password_hash FROM users WHERE email = :email");
$statement->execute(["email" => strtolower(trim((string) $_POST["email"]))]);
$user = $statement->fetch();
$loggedIn = $user !== false && password_verify((string) $_POST["password"], $user["password_hash"]);
var_dump($loggedIn);
bool(true)
Exercise 2: the greeting page
<?php
echo "<h1>Welcome, " . $_GET["name"] . "</h1>";
echo '<a href="profile.php?user=' . $_GET["name"] . '">Your profile</a>';
Show the answer
Both lines are XSS holes. The first needs HTML escaping. The second also needs the value URL-encoded, and the whole attribute HTML-escaped.
<?php
$_GET["name"] = '"><script>alert(1)</script>';
$name = (string) ($_GET["name"] ?? "");
echo "<h1>Welcome, " . htmlspecialchars($name, ENT_QUOTES, "UTF-8") . "</h1>\n";
echo '<a href="profile.php?' . htmlspecialchars(http_build_query(["user" => $name]), ENT_QUOTES, "UTF-8") . '">Your profile</a>';
<h1>Welcome, "><script>alert(1)</script></h1>
<a href="profile.php?user=%22%3E%3Cscript%3Ealert%281%29%3C%2Fscript%3E">Your profile</a>
Exercise 3: delete by link
<?php
// delete.php?id=5
$pdo->exec("DELETE FROM posts WHERE id = " . $_GET["id"]);
How many problems can you find?
Show the answer
- SQL injection (
$_GET["id"]goes into the SQL). - CSRF and unsafe method: a GET link deletes data, so it can be triggered by an image tag or a crawler.
- No login or ownership check: anyone can delete any post.
- No validation that
idis a number.
<?php
session_start();
if ($_SERVER["REQUEST_METHOD"] !== "POST") { http_response_code(405); exit; }
if (!csrfValid($_SESSION, $_POST["csrf_token"] ?? null)) { http_response_code(403); exit; }
$userId = requireLogin();
$id = filter_var($_POST["id"] ?? null, FILTER_VALIDATE_INT);
if ($id === false) { http_response_code(400); exit; }
$statement = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user");
$statement->execute(["id" => $id, "user" => $userId]);
Exercise 4: the page loader
<?php
include $_GET["page"] . ".php";
Show the answer
A local file inclusion hole. A visitor can request ?page=../config/database or similar and run or expose files. Use an allow-list.
<?php
$pages = ["home" => "pages/home.php", "about" => "pages/about.php"];
$requested = "../../etc/passwd";
echo $pages[$requested] ?? $pages["home"];
pages/home.php
Exercise 5: the avatar upload
<?php
move_uploaded_file($_FILES["avatar"]["tmp_name"], "uploads/" . $_FILES["avatar"]["name"]);
Show the answer
The visitor controls the file name and content, and the file lands inside the public folder. An attacker can upload shell.php. Fix: check the real type with finfo, allow only specific image types, use a random name with an extension you choose, and store the file outside the web root. See Secure File Uploads.
Exercise 6: the “remember me” cookie
<?php
setcookie("user_id", (string) $user["id"], time() + 86400 * 30);
// later: if (isset($_COOKIE["user_id"])) { $_SESSION["user_id"] = $_COOKIE["user_id"]; }
Show the answer
Anyone can set user_id=1 in their browser and become user 1 (or an admin). The cookie must hold a random token that is checked against a hash stored in the database. See Remember Me.