Safeguard
Security

PHP Code Check: A Security Guide

A PHP code check should catch injection, unsafe deserialization, and vulnerable Composer packages before they ship. Here is a layered approach that fits a normal PHP workflow.

Karan Patel
Platform Engineer
5 min read

A security-focused PHP code check means running static analysis over your own code and an audit over your Composer dependencies, then gating merges on what those two find. PHP gives you a lot of room to write dangerous code quickly, so the goal of any PHP code check is to make the risky patterns visible before they reach production. This guide covers the tools, the specific PHP pitfalls worth checking for, and how to check PHP in a build without slowing everyone down.

Why PHP Needs a Deliberate Check

PHP's flexibility is also its hazard. Dynamic includes, loose typing, eval, variable variables, and functions like unserialize on untrusted input all make it easy to introduce vulnerabilities that a casual review misses. Add a decade of legacy code in many PHP shops and you have a strong case for automated checking rather than relying on manual reads.

The three layers worth automating are static analysis of source, a dependency audit of the Composer tree, and a lightweight secret scan. Each catches a different class of problem.

Static Analysis: The First PHP Code Check

Start with the tools that already have wide adoption. PHPStan and Psalm both perform deep static analysis and catch type errors and dead code that often hide bugs. For security specifically, they help most when you run them at a high strictness level so unsafe assumptions surface.

composer require --dev phpstan/phpstan
vendor/bin/phpstan analyse src --level 6

Layer a security-specific ruleset on top. Semgrep has a well-maintained PHP ruleset that flags injection sinks, unsafe deserialization, and weak crypto:

semgrep --config "p/php" src/

When you check PHP this way, the patterns that matter most are user input flowing into mysqli_query or a raw PDO query string, input reaching system or exec, and untrusted data passed to unserialize. The last one is the source of many PHP object-injection issues, so treat any unserialize on request data as a finding to justify or remove.

Auditing Composer Dependencies

Your own code is only part of the risk. Composer packages carry their own vulnerabilities, and the built-in audit command is the fastest way to see them:

composer audit

This checks your installed packages against the PHP Security Advisories Database and reports known issues with severity. Run it on every build, not just when you add a package, because advisories are published for versions you already depend on. An SCA tool such as Safeguard can extend this by tracking transitive packages and showing which direct requirement pulled a vulnerable one in.

Keep composer.lock committed. Auditing a floating dependency range tells you little; auditing the exact resolved versions in the lockfile tells you what actually ships.

Checking for Injection Concretely

SQL injection remains the highest-impact PHP bug. The defensive pattern is always parameterized queries. A code check should flag string-concatenated SQL and steer developers toward prepared statements:

// flagged by a good check: concatenated input
$db->query("SELECT * FROM users WHERE id = " . $_GET['id']);

// safe: parameterized
$stmt = $db->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_GET['id']]);

Command injection follows the same logic. Prefer escapeshellarg on any argument, and avoid building shell strings from request data at all where you can. For a deeper treatment of the query side, see the guide on how to avoid SQL injection.

Wiring the Checks Into CI

A PHP code check earns trust only if it is fast and consistent. Run static analysis and the Composer audit on every pull request, and cache Composer's vendor directory between runs so you are not reinstalling each time.

- run: composer install --no-progress --prefer-dist
- run: vendor/bin/phpstan analyse src --level 6
- run: composer audit --no-dev

Fail the build on new high-severity dependency findings and on any new critical static-analysis rule. Let lower-severity items land in a report the team reviews rather than blocking every merge. This keeps the gate credible so developers do not learn to ignore it.

Handling Legacy Code

Most PHP codebases carry legacy that will never pass a strict check cleanly. Do not try to fix it all at once. Use a baseline: PHPStan and Psalm both support generating one that records existing findings so the build only fails on new ones.

vendor/bin/phpstan analyse --generate-baseline

From there, every new pull request must be clean, and you burn down the baseline opportunistically. This lets you check PHP strictly on new work without a multi-month cleanup blocking the pipeline.

FAQ

What is the fastest way to start a PHP code check?

Run composer audit for dependencies and Semgrep with the p/php ruleset for code. Both take minutes to set up and immediately surface the highest-impact issues.

Does composer audit check transitive dependencies?

Yes. It evaluates every package in your resolved lockfile, including transitive ones, against the advisory database. That is why keeping composer.lock committed matters.

How do I check PHP without breaking the build on legacy issues?

Generate a baseline with PHPStan or Psalm. It records current findings so the gate only fails on newly introduced problems, letting you tighten checks on new code immediately.

Are static analysis tools enough on their own?

No. Static analysis checks the code you wrote but not the CVEs in your dependencies. Pair it with a Composer audit or a dedicated SCA tool for full coverage.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.