PHP application security is mostly about disciplined handling of untrusted input, and the majority of real PHP breaches trace back to a small set of well-understood mistakes rather than exotic attacks. PHP still powers a huge share of the web — WordPress alone runs on well over 40% of all websites — so the language attracts constant probing. The good news is that modern PHP (8.x) plus a few consistent habits closes most of the door. This guide walks through the controls that actually move the needle, with the code patterns that get them right.
Stop SQL injection with prepared statements, always
SQL injection remains the highest-impact bug class in PHP applications, and it is entirely preventable. The rule is simple: never concatenate user input into a query string. Use parameterized queries through PDO or mysqli, every time, with no exceptions for "internal" or "trusted" inputs.
Here is the wrong pattern, the one that shows up in breach postmortems:
// Vulnerable: user input concatenated into SQL
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
$result = $db->query($sql);
An attacker submits ' OR '1'='1 and the query logic collapses. The fix is a prepared statement, where the input is sent to the database separately from the query structure and can never be interpreted as SQL:
// Safe: parameterized query with PDO
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch();
If you use a query builder or ORM (Eloquent, Doctrine), it does this for you — but only if you use its parameter binding rather than dropping to raw SQL with string interpolation. Audit every DB::raw(), ->query(), and $conn->query() in the codebase; those are where the concatenation sneaks back in.
Neutralize XSS by escaping on output
Cross-site scripting happens when user-controlled data lands in an HTML page without being escaped, letting an attacker run JavaScript in a victim's browser. The defensive rule is to escape at the point of output, contextually, and to treat all stored and reflected data as untrusted.
In raw PHP, escape with htmlspecialchars using the right flags:
echo htmlspecialchars($comment, ENT_QUOTES | ENT_HTML5, 'UTF-8');
Templating engines like Twig and Blade auto-escape by default, which is a strong reason to use them rather than echoing variables directly. The danger with those engines is the escape hatch — Twig's |raw filter and Blade's {!! !!} syntax disable escaping. Every use of those is a place to prove the content is genuinely safe HTML, not user input. Add a Content-Security-Policy header as a second layer so that even a missed escape has a harder time executing an injected script.
Handle sessions and authentication carefully
Session handling is where a lot of PHP apps quietly leak. A few concrete controls:
Regenerate the session ID on privilege change, especially at login, to defeat session fixation:
session_regenerate_id(true);
Set cookie flags so the session cookie cannot be read by JavaScript or sent over plain HTTP:
session_set_cookie_params([
'httponly' => true,
'secure' => true,
'samesite' => 'Lax',
]);
For passwords, use PHP's built-in hashing rather than rolling your own. password_hash with the default algorithm and password_verify handle salting and work factors for you:
$hash = password_hash($password, PASSWORD_DEFAULT);
// ...
if (password_verify($input, $hash)) { /* authenticated */ }
Never store passwords with md5, sha1, or any fast hash. If you inherited a codebase that does, rehash on next successful login.
Lock down file uploads and includes
File upload handling is a classic PHP foot-gun because a web server that executes .php will happily run an uploaded script if it lands in a served directory. Validate the file type by content, not just extension, store uploads outside the web root or in a location configured not to execute PHP, and generate your own filenames rather than trusting the client's. Reject anything that does not match an allow-list of expected types.
Related, avoid dynamic file inclusion driven by user input. A pattern like include($_GET['page'] . '.php') is a local file inclusion vulnerability waiting to happen. Map user-facing routes to a fixed allow-list of files instead of building paths from request data.
Manage your Composer dependencies
Modern PHP applications pull in dozens of packages through Composer, and those dependencies carry their own vulnerabilities. Your carefully written code does not help if a transitive package has a known deserialization or injection bug. Two habits keep this in check.
Run the built-in audit regularly, which checks your locked dependencies against known advisories:
composer audit
And keep composer.lock committed and updated on a schedule rather than drifting for months. For continuous coverage across a fleet of PHP services, an SCA tool such as Safeguard can flag vulnerable Composer packages, including transitive ones that composer audit might surface only after you dig into the tree. Dependency risk is now a first-class part of PHP application security, not an afterthought — the application security scanning guide covers how it fits with the code-level controls above.
Configure PHP and the server defensively
A few environment settings matter as much as code. In production, set display_errors = Off so stack traces never reach users, keep expose_php = Off to avoid advertising your version, and keep PHP itself current — running a supported 8.x release means you get security patches and modern defaults. Disable dangerous functions you do not need (eval, exec, system, shell_exec) via disable_functions if your application has no legitimate use for them. These are cheap changes that shrink the attack surface before an attacker even reaches your code.
FAQ
What is the most common PHP security vulnerability?
Injection, and SQL injection in particular, remains the highest-impact issue. It is caused by concatenating untrusted input into queries and is fully preventable with parameterized queries via PDO or mysqli.
How do I prevent XSS in a PHP application?
Escape user-controlled data at the point of output using htmlspecialchars with ENT_QUOTES, or rely on the auto-escaping in a templating engine like Twig or Blade. Add a Content-Security-Policy header as a defense-in-depth layer, and scrutinize every use of raw/unescaped output.
Is PHP less secure than other languages?
Not inherently. PHP's reputation comes from a large base of old, unmaintained applications and insecure legacy patterns. Modern PHP 8.x with prepared statements, password_hash, proper session flags, and dependency scanning is as secure as any comparable stack.
How do I check my PHP dependencies for vulnerabilities?
Run composer audit against your composer.lock to check installed packages against known advisories, and use an SCA tool for continuous monitoring that also traces transitive dependencies across multiple services.