PHP code analysis is the practice of inspecting PHP source — statically without running it, and dynamically while it executes — to find security flaws, bugs, and quality problems before they reach production. Because PHP is dynamically typed, forgiving of loose comparisons, and historically full of dangerous built-in functions, automated analysis catches a class of issues that code review alone routinely misses.
This guide covers what the two kinds of analysis do, the tools worth running, and the PHP-specific patterns that deserve special attention.
Static versus dynamic analysis
Static analysis reads your code without executing it. It parses the source into an abstract syntax tree and traces how data flows from inputs (request parameters, files, database rows) to sensitive operations (SQL queries, shell commands, file paths). When untrusted data reaches a dangerous sink without sanitization, it flags a potential vulnerability. This is where you catch injection, path traversal, and insecure deserialization.
Dynamic analysis exercises the running application and watches its behavior — what queries it sends, which files it touches, how it responds to malformed input. It finds issues that only appear at runtime and confirms whether a statically flagged path is actually reachable. The two are complementary: static analysis has full code coverage but produces false positives; dynamic analysis has no false positives on what it triggers but only sees code paths it actually reaches.
Static analysis tools for PHP
A practical PHP analysis stack layers a few tools, each catching different things.
PHPStan and Psalm are type-focused static analyzers. Their primary job is correctness — undefined variables, wrong argument types, dead code — but stricter levels surface security-relevant bugs like unvalidated input reaching a query. Psalm also has taint analysis you can enable explicitly:
composer require --dev vimeo/psalm
vendor/bin/psalm --taint-analysis
Taint mode marks superglobals like $_GET and $_POST as tainted sources and reports when that data reaches a sink such as PDO::query or echo without escaping.
PHP_CodeSniffer enforces coding standards and can flag risky constructs. Semgrep runs pattern-based rules and ships a PHP security ruleset that catches common anti-patterns quickly:
semgrep --config "p/php" src/
For dependency risk rather than your own code, a software composition analysis tool inspects your composer.lock and reports known CVEs in the packages you pull in — an entirely separate concern from analyzing the code you wrote, and one covered in our SCA product overview.
PHP-specific patterns to hunt
Certain vulnerability classes show up disproportionately in PHP because of language and ecosystem history.
SQL injection from string interpolation. PHP makes it trivially easy to build a query with a variable in a double-quoted string:
// Vulnerable
$sql = "SELECT * FROM users WHERE email = '$email'";
$db->query($sql);
// Safe: prepared statement with a bound parameter
$stmt = $db->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
Analysis flags the interpolated query as a tainted flow into a database sink. The fix is always parameterized queries via PDO or mysqli.
Command injection via exec, system, shell_exec, and backticks. Any of these with unsanitized input is a remote code execution risk. Static tools flag input reaching these functions; the fix is escapeshellarg() on every argument, or better, avoiding shell calls entirely.
Insecure deserialization. Calling unserialize() on attacker-controlled data lets an attacker instantiate arbitrary objects and trigger PHP object injection through magic methods like __wakeup and __destruct. Analyzers flag unserialize() on tainted input. Prefer json_decode() for untrusted data, and if you must use unserialize, pass ['allowed_classes' => false].
Local and remote file inclusion. include and require with a variable path let an attacker load arbitrary files. Combined with PHP's stream wrappers, this has historically escalated to code execution. Validate against an allowlist of permitted files rather than sanitizing the path.
Loose comparison bugs. PHP's == performs type juggling, so "0e123" == "0e456" can evaluate true because both look like scientific-notation zero. This has broken password-hash comparisons. Use === and hash_equals() for security-sensitive comparisons — a rule you can encode as a custom Semgrep pattern.
Fitting analysis into CI
Analysis only helps if it runs consistently, so wire it into your pipeline and make it a gate:
# GitHub Actions excerpt
- name: Static analysis
run: |
vendor/bin/phpstan analyse --level 6 src/
vendor/bin/psalm --taint-analysis
semgrep --config "p/php" --error src/
Start with a baseline. A mature PHP codebase will light up with hundreds of findings on the first run, and blocking every one on day one is unworkable. PHPStan and Psalm both support baseline files that record existing findings so the gate only fails on new issues, letting you ratchet up strictness over time without a big-bang cleanup.
Triaging the output
Not every finding is a real vulnerability. Static analysis produces false positives — flows it believes are dangerous that are actually guarded by validation it could not see, or code that is unreachable in practice. Budget time to triage: confirm the input is genuinely attacker-controllable, trace whether sanitization exists on the path, and mark false positives so they do not resurface. Feeding real exploitability signal back into your ruleset is what keeps developers trusting the tool instead of ignoring it.
FAQ
What is the difference between PHP code analysis and a linter?
A linter checks style and simple correctness — indentation, unused variables, missing semicolons. Code analysis for security goes deeper, tracing data flow from untrusted inputs to dangerous operations to find exploitable vulnerabilities. PHPStan and Psalm blur the line, doing both type checking and security-relevant taint analysis at higher levels.
Can static analysis find every PHP vulnerability?
No. Static analysis excels at injection and data-flow bugs but struggles with logic flaws, broken access control, and issues that depend on runtime state or configuration. Combine it with dynamic testing and manual review for meaningful coverage; no single technique catches everything.
Is PHPStan or Psalm better for security?
They overlap heavily. Psalm has more mature built-in taint analysis for security specifically, while PHPStan has a larger ecosystem of extensions and is often faster to adopt. Many teams run both, or run one plus Semgrep for pattern-based security rules. Pick based on which fits your workflow and raise the strictness level over time.
How do I reduce false positives?
Raise strictness gradually with a baseline file, write project-specific rules that understand your sanitization helpers, and consistently mark confirmed false positives so they are suppressed. Over a few weeks the signal-to-noise ratio improves sharply as the tool learns your codebase's conventions.