The PHP Security Vulnerabilities Still Causing Business Website Breaches
Most PHP security articles are written for developers who have not seen a breach. This article is written from the perspective of someone who has cleaned up after them. The vulnerabilities that cause real-world breaches in business websites are not exotic. They are the same vulnerabilities that have been documented for twenty years and are still present in production code because the basics are not being followed.
This article covers the specific security problems that actually appear in PHP business websites: SQL injection, cross-site scripting, session security, file upload vulnerabilities, password handling, and CSRF. It is not a comprehensive security guide. It is a practical checklist for the vulnerabilities that consistently appear in live PHP applications, how they work, and how to check if your site has them.
SQL Injection: The Vulnerability That Still Dominates Breach Reports
SQL injection has been documented since 1998. The OWASP Top 10 has listed it in every edition since the list began. It is understood better than almost any other web vulnerability. It is still the most common root cause of PHP website breaches. The reason is not that the fix is unknown. The reason is that PHP applications consistently build SQL queries by concatenating user input without proper escaping.
The mechanism is straightforward. When user input is included in a SQL query without escaping, an attacker can alter the query's logic. A normal query might retrieve user data based on an email address. An attacker submitting carefully crafted input can change the query's structure entirely, returning data the developer never intended to expose.
The underlying vulnerability applies to any unescaped SQL query in PHP, regardless of the framework or application type. Understanding the broader context of how this vulnerability ranks among common web application risks helps prioritisation decisions. The OWASP Top 10 for business web applications provides a useful overview of where SQL injection sits alongside other risks you may also encounter in your PHP codebase.
In PHP specifically, the most common SQL injection vector is direct use of $_POST or $_GET values in query strings without any escaping. Functions like mysqli_real_escape_string() exist specifically to handle this, but developers often use them incorrectly or forget them entirely on some input paths while using them on others.
Consider a typical PHP login handler that builds a query by concatenating user input directly:
$email = $_POST['email'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE email = '$email' AND password = '$password'";
$result = mysqli_query($conn, $query);
If an attacker submits a specially crafted string as the email, the query becomes something unexpected. The password check may be bypassed entirely, and the attacker gains access to the first user account in the table, often an administrator account.
The correct approach uses prepared statements, which separate query structure from user data and make injection structurally impossible:
$stmt = $conn->prepare("SELECT * FROM users WHERE email = ? AND password = ?");
$stmt->bind_param("ss", $_POST['email'], $_POST['password']);
$stmt->execute();
With prepared statements, user input is never interpreted as SQL code, regardless of what characters it contains. This is the only safe approach. Manual escaping functions can work as a belt-and-braces measure but should never be the primary defence because it is easy to miss on some inputs.
Blind SQL injection is a variant that applies when the application does not return query results in its response. An attacker determines whether a condition is true or false based on how long the page takes to load or whether content changes. If your application has SQL injection vulnerabilities, automated tools can detect and exploit them in minutes.
Cross-Site Scripting: Stealing Sessions and Defacing Pages
Cross-site scripting (XSS) occurs when user input is reflected in a web page without proper encoding, allowing an attacker to inject JavaScript that runs in the victim's browser. Unlike SQL injection, which attacks the server, XSS attacks the users of the site. Both are common in PHP applications that do not encode output correctly.
Stored XSS is the most dangerous variant. User input is saved to a database and served to all visitors without encoding. A common scenario is a comment form that does not sanitise HTML. If an attacker submits a comment containing a script tag, every visitor who loads that comment page executes the attacker's JavaScript in their browser.
Reflected XSS occurs when user input is echoed back in a response without encoding. The classic example is a search result page that displays the search term without escaping it. An attacker crafts a URL containing a script and tricks a victim into clicking it.
DOM-based XSS is more subtle. The attack payload never reaches the server. Instead, JavaScript on the page reads user input from the URL fragment or other sources and writes it to the page DOM without sanitisation.
The fix for output is context-appropriate encoding. HTML output must escape special characters. PHP's htmlspecialchars() function handles this:
echo htmlspecialchars($userInput, ENT_QUOTES | ENT_HTML5, 'UTF-8');
For a practical guide specifically covering XSS prevention using PHP output encoding techniques, there are detailed examples for different contexts where user input appears in your pages.
Content Security Policy (CSP) headers provide a secondary defence. A correctly configured CSP prevents inline scripts from executing even if an XSS vulnerability exists:
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-random123'");
File Upload Vulnerabilities: When Your Upload Form Executes Code
File upload forms are common in business websites: profile photo uploads, document submissions, image galleries. They are also one of the most frequently exploited attack surfaces. A misconfigured file upload handler can allow an attacker to upload and execute arbitrary PHP code on your server.
The attack works like this: an attacker uploads a PHP file instead of an image. If the upload process only checks the file extension or the MIME type sent by the browser, the file is saved with a .php extension. The attacker then visits the uploaded file's URL, and the server executes it as PHP code. At this point the attacker has full server access.
The correct approach to file uploads involves multiple checks, none of which alone is sufficient. Never trust the filename provided by the client. Generate a new random filename. Store uploaded files outside the web root so they cannot be accessed directly via a URL. If files must be accessible, serve them through a PHP script that validates the user's access rights before reading and outputting the file content.
- Validate file type server-side: Check the actual file content, not just the extension or MIME type.
- Store outside web root: Uploaded files should not be directly accessible via URL.
- Generate random filenames: Never use the user-provided filename for storage.
- Use a delivery script: Serve files through PHP with proper access control checks.
Password Handling: Why MD5 Is Still Being Used in 2024
Password storage failures have caused some of the largest breaches in history. The problem is straightforward: many PHP applications still use hash functions designed for data integrity verification, not password storage. MD5 and SHA-1 are fast hash functions. Modern GPUs can compute billions of MD5 hashes per second. A list of one million common passwords can be matched against an entire password database in seconds.
PHP's password_hash() function uses bcrypt by default and automatically handles salt generation:
$hash = password_hash($password, PASSWORD_DEFAULT);
if (password_verify($password, $hash)) {
// login successful
}
The PASSWORD_DEFAULT constant uses the current strongest algorithm supported by PHP (currently Argon2id). When PHP upgrades its default algorithm, existing hashes can be automatically upgraded by checking password_needs_rehash() on login:
if (password_verify($password, $hash)) {
if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
$newHash = password_hash($password, PASSWORD_DEFAULT);
// update hash in database
}
return true;
}
A complete PHP security checklist covering input validation, output encoding, SQL injection prevention, and password security gives you a broader view of the protections needed across a full application.
Session Security: Hijacking and Fixation
Session hijacking occurs when an attacker obtains a valid session identifier and uses it to impersonate the associated user. In PHP, session identifiers are typically stored in a cookie named PHPSESSID. Session fixation is a related attack where the attacker sets the session identifier before the victim logs in.
Configure session security settings in php.ini or at runtime:
ini_set('session.cookie_httponly', 1); // JavaScript cannot read the session cookie
ini_set('session.cookie_secure', 1); // Cookie only sent over HTTPS
ini_set('session.use_strict_mode', 1); // Refuse unrecognised session IDs
ini_set('session.cookie_samesite', 'Strict'); // Cookie not sent in cross-site requests
The httponly flag prevents JavaScript from accessing the session cookie, which blocks the most common XSS-based session theft vector. Regenerate the session ID after any privilege change such as login:
session_regenerate_id(true); // true destroys the old session
CSRF: Forging Requests on Behalf of Your Users
Cross-site request forgery (CSRF) tricks a logged-in user into submitting a request they did not intend. Because the user's browser automatically includes their session cookie with every request to your domain, the forged request is authenticated by virtue of the victim's existing session.
The classic attack scenario: a user is logged into your application in one tab while browsing a malicious website in another tab. The malicious page contains an auto-submitting form that POSTs to your application's endpoints. CSRF tokens prevent this. Every state-changing request should include a unique, unpredictable token that the server verifies before processing:
// Generate token when showing the form
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
echo '';
// Verify token when processing the form
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
http_response_code(403);
exit('Invalid CSRF token');
}
The hash_equals() function provides timing-safe comparison to prevent timing attacks.
Remote Code Execution: When Deserialisation Becomes a Weapon
PHP's unserialize() function is a direct path to remote code execution. It takes a string and reconstructs a PHP object from it. If an attacker can control the input to unserialize(), they can craft a payload that exploits PHP's object autoloading mechanism to execute arbitrary code.
The correct approach is simple: never use unserialize() on data that comes from untrusted sources. Use JSON instead, which does not support object deserialisation. If you must handle serialised PHP data, use json_encode() and json_decode() throughout your application.
// Unsafe - never use on untrusted input
$data = unserialize($_POST['serialized_data']);
// Safe alternative
$data = json_decode($_POST['json_data'], true);
What to Do If Your PHP Site Is Already Compromised
If you discover a breach, the priority is containment before investigation. Take the site offline immediately by taking the server offline or blocking all traffic to it. Do not try to clean and keep it live while investigating. An attacker with persistent access will simply re-enter through a backdoor you missed.
- Take the site offline immediately: Block all traffic or take the server down. Do not attempt to clean while keeping it live.
- Audit access logs: Understand how the attacker gained access. Identify the exploited vulnerability.
- Fix the vulnerability before restoring: Do not bring the site back online until the entry point is closed.
- Restore from a known-good backup: Use a backup that was taken before the breach date.
- Change all credentials: Database passwords, API keys, session secrets, any credentials stored on the server.
- Document everything: If the breach involved customer data, legal obligations under GDPR apply. The ICO expects evidence of a thorough investigation and remediation.
Building a Sustainable PHP Security Practice
Security is not a one-time fix. PHP applications require ongoing attention as new vulnerabilities emerge and your codebase evolves. Regular code reviews focusing on how user input flows through your application help catch issues before they reach production.
Keep your PHP version current. Each minor release includes security patches, and running an unsupported version means you are missing fixes for known vulnerabilities. The same applies to libraries and frameworks in your stack.
Good IT documentation practices support security maintenance. Keeping runbooks for your PHP applications, documenting the security controls in place, and maintaining a record of dependencies and their update status makes ongoing maintenance manageable rather than overwhelming.
Before deploying any PHP application that handles user data or authentication, run through the specific vulnerabilities covered here. SQL injection, XSS, CSRF, weak session configuration, unsafe deserialisation, and poor file upload handling represent the most common paths attackers use to compromise PHP business websites.