
PHP still powers a huge share of the web, from small sites to some of the largest platforms online. That reach makes secure PHP development a practical necessity rather than an afterthought. The good news is that PHP security is very learnable: most incidents trace back to a short list of well-understood mistakes, and each has a reliable, well-supported fix. This guide walks through the vulnerabilities that matter most and the secure coding in PHP practices that neutralize them.
The mindset that ties everything together is simple: treat every piece of input as untrusted until proven otherwise, and treat every piece of output as dangerous until it is properly encoded for its destination.
SQL injection and prepared statements
SQL injection remains one of the highest-impact flaws in PHP applications, and it appears whenever user input is concatenated directly into a query string. The fix is to stop mixing data and code. Use PDO with parameterized queries so the database engine, not string concatenation, decides what is data.
Prepare a statement like $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?'); and then $stmt->execute([$email]);. The user value is bound as a parameter and can never alter the query structure. Make PDO your default database layer, enable exceptions with PDO::ATTR_ERRMODE, and avoid emulated prepares so that real server-side binding is used.
File inclusion (LFI and RFI)
PHP's include and require are powerful, and that power is exactly the problem when user input reaches them. Passing a request parameter into include($_GET['page'] . '.php') opens the door to local file inclusion (reading or executing files already on the server) and, in misconfigured environments, remote file inclusion.
Never build an include path from user input directly. Instead, use an allowlist: map a small set of known-good keys to fixed file paths, and reject anything not on the list. Keep allow_url_include and allow_url_fopen disabled unless you have a specific, reviewed reason to change them.
Command injection
Functions like system, exec, shell_exec, passthru, and the backtick operator hand input to the operating system shell. If any user-controlled value flows into them unescaped, an attacker can chain their own commands. Wherever possible, avoid shelling out at all and use a native PHP function or library instead. When you genuinely must call an external program, escape arguments with escapeshellarg() and the command name with escapeshellcmd(), and validate the input against a strict expected format first.
Insecure deserialization
Calling unserialize() on untrusted input is dangerous because PHP will instantiate objects and trigger magic methods during the process, which attackers can leverage into object-injection and remote code execution chains. The rule is direct: never run unserialize() on data that came from a user, a cookie, or any external source. Prefer json_decode() for interchange, since it produces plain data structures rather than live objects. If you must accept serialized input, pass the allowed_classes option to restrict what can be instantiated.
Cross-site scripting (XSS)
XSS happens when user-supplied data is written into an HTML page without encoding, letting an attacker's script run in a victim's browser. Output encoding is the answer. Encode dynamic values for their context with htmlspecialchars($value, ENT_QUOTES, 'UTF-8') before printing them into HTML. For a robust defense, pair output encoding with a Content Security Policy header and use a templating engine that auto-escapes by default.
Cross-site request forgery (CSRF)
CSRF tricks an authenticated user's browser into submitting a request they did not intend. Defend state-changing requests with anti-CSRF tokens: generate a cryptographically random token, store it in the session, embed it as a hidden field in every form, and verify it on submission with a constant-time comparison such as hash_equals(). Setting cookies with the SameSite attribute adds a valuable second layer.
Type juggling and loose comparisons
PHP's loose comparison operator (==) performs type coercion that can produce surprising results, which becomes a security problem in checks like token or hash comparison. Using == where two differently typed values may be treated as equal can let an attacker bypass a check. Always use strict comparison (===) for security decisions, and use purpose-built functions like hash_equals() for comparing secrets and password_verify() for passwords.
Session and configuration hardening
Many classic PHP problems were configuration problems. The register_globals era is long gone, but its lesson stands: insecure defaults cause breaches. Keep display_errors off in production, log errors instead, and set session cookies with the HttpOnly, Secure, and SameSite flags. Regenerate the session ID on login with session_regenerate_id(true), store secrets in environment variables rather than in source, and keep PHP itself patched to a supported version.
Dependency risk with Composer
Modern PHP applications pull in dozens of packages through Composer and Packagist, and each dependency is code you are trusting. A vulnerability in a transitive package is your vulnerability. Commit your composer.lock file so builds are reproducible, review what you add, remove packages you no longer use, and run composer audit to check installed packages against known advisories. Automated software composition analysis takes this further by continuously flagging vulnerable and outdated components across your whole dependency tree.
Bring it into CI: SAST and SCA
Manual review is valuable but does not scale. Static application security testing (SAST) analyzes your PHP source for insecure patterns, while SCA watches your dependencies. Wiring both into your continuous integration pipeline means every commit and pull request is checked automatically, so issues are caught while they are cheap to fix rather than after deployment. If you are weighing where each technique fits, our SAST vs DAST comparison breaks down the trade-offs, and these tools sit naturally inside a broader secure software development lifecycle.
A quick secure PHP checklist
- Use PDO parameterized queries for every database interaction.
- Encode all output with
htmlspecialchars()for its context. - Allowlist any dynamic
include/requirepaths. - Escape shell arguments with
escapeshellarg(), or avoid shelling out entirely. - Never
unserialize()untrusted input; preferjson_decode(). - Use
===andhash_equals()for security-sensitive comparisons. - Protect every state-changing form with a CSRF token.
- Harden session cookies and keep production error display off.
- Pin and audit Composer dependencies.
- Run SAST and SCA on every build.
How Rainforest helps
Rainforest is built to make secure PHP development part of your normal workflow rather than a separate chore. Our application security testing platform scans your code for the vulnerability classes above and integrates directly into CI/CD, so findings show up in the pull request with clear guidance on where the issue is and how to remediate it. On the dependency side, software composition analysis tracks your Composer packages and alerts you when a component carries a known vulnerability. Grounding your program in the OWASP risk categories keeps your team focused on what attackers actually target.
Secure applications come from good habits reinforced by good tooling. If you would like to see how Rainforest fits your PHP stack, book a demo and we will walk through it with you.
Frequently asked questions
Is PHP secure?
PHP can be very secure. The language and its modern ecosystem provide strong tools, such as PDO, password_hash(), and auto-escaping templates. Most PHP breaches come from insecure coding practices and outdated dependencies, not from the language itself. Staying on a supported PHP version and following the practices in this guide addresses the large majority of real-world risk.
What are common PHP security vulnerabilities?
The most frequent are SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), local and remote file inclusion (LFI/RFI), command injection, and insecure deserialization. Loose-comparison (type juggling) bugs and weak session or configuration settings round out the list. All of them are preventable with well-established techniques.
How do I prevent SQL injection in PHP?
Use PDO prepared statements with bound parameters for every query so that user input is always treated as data and never as executable SQL. Enable exception-mode error handling, disable emulated prepares to force true server-side binding, and validate input against the format you expect before it reaches the database.
How do I manage PHP dependency risk?
Treat Composer dependencies as part of your attack surface. Commit composer.lock for reproducible builds, review and minimize what you add, and run composer audit regularly. For continuous coverage, use software composition analysis to automatically detect vulnerable and outdated packages across your direct and transitive dependencies.
How do I scan PHP code for vulnerabilities?
Combine static application security testing (SAST) to analyze your source code with software composition analysis (SCA) to check your dependencies, and run both automatically in your CI/CD pipeline. Rainforest provides both and reports findings directly in your pull requests so issues are fixed before they ship.

Written by
Bruno Baldo
CMO
Um pouco de marketing e um pouco de curiosidade e temos a receita pra criar um apaixonado por cyber!

Secure Software Development: Practices for a Secure SDLC
Learn secure software development: how to build a secure SDLC phase by phase, apply secure coding practices, and shift security left in CI/CD.

SAST vs DAST: The Differences and When to Use Each
SAST vs DAST explained: how static and dynamic application security testing differ, when to use each, and why mature AppSec programs run both.
