๐ PHP Vulnerability Examples and Fixes
Use this page when: reviewing PHP services, old MVC applications, upload paths, or utility endpoints that mix user input with shell, filesystem, or SQL operations.
How to read these examples
- Vulnerable snippet shows the unsafe habit.
- Safer pattern shows the direction you want in production code.
- Why it matters ties the defect to attacker value and business impact.
- Review cue is phrased so it can become a pull-request comment or checklist item.
Example 1 โ SQL injection through string concatenation
Vulnerable snippet
$email = $_GET['email'];
$sql = "SELECT id, role FROM users WHERE email = '$email'";
$result = $pdo->query($sql);
$user = $result->fetch();
Safer pattern
$stmt = $pdo->prepare("SELECT id, role FROM users WHERE email = :email");
$stmt->execute(['email' => $_GET['email']]);
$user = $stmt->fetch();
Why it matters
- String-built queries let attacker-controlled input change the meaning of the SQL statement.
Business impact
- Account enumeration, full data exposure, privilege escalation, or destructive queries against production data.
Review cue
- Do not concatenate request data into SQL. Require prepared statements or a safe query builder for all value binding.
Example 2 โ Reflected or stored XSS in output rendering
Vulnerable snippet
$comment = $_POST['comment'];
echo "<div class='comment'>$comment</div>";
Safer pattern
$comment = $_POST['comment'] ?? '';
echo "<div class='comment'>" . htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . "</div>";
Why it matters
- Raw HTML output turns user-controlled content into executable script in the victim browser.
Business impact
- Session theft, account takeover, admin-action forgery, phishing overlays, and supply-chain-like browser compromise.
Review cue
- For every rendering context, demand context-appropriate output encoding or safe templating helpers instead of raw echo.
Example 3 โ Unsafe file upload that trusts file name and extension
Vulnerable snippet
$name = $_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'], __DIR__ . '/public/uploads/' . $name);
echo 'uploaded';
Safer pattern
$allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);
if (!isset($allowed[$mime])) {
http_response_code(400);
exit('unsupported file type');
}
$random = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$target = '/srv/app-uploads/' . $random; // outside web root
move_uploaded_file($_FILES['file']['tmp_name'], $target);
Why it matters
- Original names, fake extensions, and web-root storage allow web shells, polyglots, overwrite attacks, and unsafe direct retrieval.
Business impact
- Remote code execution, malware distribution, storage abuse, or public leakage of sensitive files.
Review cue
- Require type allowlists, generated names, size limits, storage outside web root, and separate retrieval controls.
Example 4 โ Broken object-level authorization (IDOR)
Vulnerable snippet
$invoiceId = (int) $_GET['invoice_id'];
$stmt = $pdo->prepare("SELECT * FROM invoices WHERE id = :id");
$stmt->execute(['id' => $invoiceId]);
$invoice = $stmt->fetch();
Safer pattern
$invoiceId = (int) $_GET['invoice_id'];
$userId = $_SESSION['user_id'];
$stmt = $pdo->prepare(
"SELECT * FROM invoices WHERE id = :id AND owner_user_id = :user_id"
);
$stmt->execute(['id' => $invoiceId, 'user_id' => $userId]);
$invoice = $stmt->fetch();
if (!$invoice) {
http_response_code(404);
exit('not found');
}
Why it matters
- Looking up objects by ID without ownership or role scope means any authenticated user can pivot into another userโs records.
Business impact
- Cross-tenant data disclosure, unauthorized financial actions, privacy incidents, and support/admin trust breakdown.
Review cue
- Every record fetch for sensitive data should prove scope: owner, tenant, team, or explicit delegated permission.
Example 5 โ Command injection through shell_exec
Vulnerable snippet
$term = $_GET['term'];
$output = shell_exec("grep -R $term /srv/docs");
echo nl2br($output);
Safer pattern
$term = $_GET['term'] ?? '';
if (!preg_match('/^[a-zA-Z0-9._ -]{1,40}$/', $term)) {
http_response_code(400);
exit('bad search term');
}
$matches = [];
foreach (glob('/srv/docs/*.txt') as $file) {
foreach (file($file, FILE_IGNORE_NEW_LINES) as $line) {
if (str_contains($line, $term)) {
$matches[] = $line;
}
}
}
echo nl2br(htmlspecialchars(implode("
", $matches), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'));
Why it matters
- Shell interpolation makes metacharacters part of the command, not just part of the search term.
Business impact
- Remote code execution, data exfiltration, lateral movement, and compromise of the host or its secrets.
Review cue
- If user input reaches a shell, stop. Prefer native language functions, strict validation, and no shell expansion.
Related pages
- Backend Service Security Guides by Stack
- Web Application Security Review and Architecture Playbook
- Repository Secret Scanning
Author attribution: Ivan Piskunov, 2026 - Educational and defensive-engineering use.