Safeguard
Security

How to Test PHP Code Securely (Testar PHP the Right Way)

A practitioner's guide to testing PHP code for security bugs, from unit tests that assert on input handling to SAST and dependency scanning in CI.

Priya Mehta
DevSecOps Engineer
6 min read

To testar PHP code securely, you pair ordinary functional tests with security-focused checks: unit tests that assert on how input is escaped and validated, static analysis that flags injection sinks, and dependency scanning that catches vulnerable Composer packages before they ship. Writing tests that only prove "the happy path works" leaves the exact bugs attackers look for wide open. This guide walks through a layered approach to testar codigo PHP so that correctness and security are checked in the same pipeline.

Start with the test pyramid, not the security scanner

A common mistake is to reach for a scanner first and skip fast, deterministic unit tests. Do the opposite. PHPUnit is still the workhorse for PHP, and it should carry most of your assertions.

Install it per-project through Composer so the version is pinned:

composer require --dev phpunit/phpunit ^11
./vendor/bin/phpunit --version

A security-minded unit test does more than check a return value. It asserts on the shape of output that crosses a trust boundary. For example, if a function renders user-supplied text into HTML, the test should prove the output is encoded:

public function testUserInputIsHtmlEncoded(): void
{
    $html = render_comment('<script>alert(1)</script>');
    $this->assertStringNotContainsString('<script>', $html);
    $this->assertStringContainsString('&lt;script&gt;', $html);
}

That single assertion catches a whole class of stored XSS regressions. When you testar PHP this way, security expectations live in the same suite developers already run before every commit.

Test input validation and boundaries explicitly

Injection bugs almost always trace back to input that was trusted when it should not have been. Write tests that feed hostile input and assert the code rejects or neutralizes it.

For SQL, the defensive pattern is parameterized queries via PDO. Your test should prove the query layer never string-concatenates user data:

public function testLookupUsesBoundParameters(): void
{
    $repo = new UserRepository($this->pdo);
    $repo->findByEmail("' OR '1'='1");
    // The malicious string must be treated as a literal value,
    // returning zero rows rather than the whole table.
    $this->assertCount(0, $repo->lastResult());
}

Do the same for file paths (directory traversal with ../), for command arguments (shell metacharacters), and for deserialization. The point of testing codigo PHP for security is to encode these attack classes as failing tests first, then make them pass.

Add static analysis to catch what tests miss

Unit tests only exercise the paths you thought to write. Static application security testing (SAST) reads the whole codebase and flags dangerous patterns you never called in a test. For PHP, three tools cover most ground:

  • PHPStan or Psalm for type-level analysis. Running at a high level surfaces null-safety and type-confusion bugs that often underlie security flaws.
  • A dedicated taint-analysis pass. Psalm ships a --taint-analysis mode that traces user input from source to sink and reports where an unescaped value reaches an echo, a SQL string, or exec().
./vendor/bin/psalm --taint-analysis

Taint analysis is the closest thing PHP has to an automatic injection detector. It will not replace your tests, but it finds the sink you forgot existed. If you want a broader treatment of static analysis across languages, our Academy has a walkthrough of how SAST and taint tracking fit together.

Scan your Composer dependencies

Your own code is only part of the attack surface. A typical PHP app pulls dozens of transitive packages through Composer, and a vulnerability in any of them is your vulnerability. Composer ships a built-in audit:

composer audit

This checks your installed versions against known advisories. Treat a non-zero exit code as a build failure. For deeper visibility, including transitive dependencies and license issues, software composition analysis fills the gap. An SCA tool such as Safeguard can flag a vulnerable package that your direct dependencies pulled in three levels down, which composer audit alone can miss in complex trees.

Keep composer.lock committed. Without it, two developers can resolve different versions and your security tests pass on one machine while a vulnerable package ships from another.

Wire everything into CI

Local testing is optional until it runs on every push. A minimal GitHub Actions job that runs the full security-aware suite looks like this:

name: test
on: [push, pull_request]
jobs:
  php:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
      - run: composer install --no-progress
      - run: ./vendor/bin/phpunit
      - run: ./vendor/bin/psalm --taint-analysis
      - run: composer audit

Each step is a gate. A failing assertion, a tainted data flow, or a vulnerable dependency stops the merge. That is the whole payoff of learning to testar PHP with security in mind: the checks run without anyone remembering to run them.

Measure coverage, but measure the right thing

Line coverage tells you which code ran during tests, not whether the security-critical paths were exercised. A file can hit 95% coverage while the one branch that handles malformed input stays untested. When you review coverage reports, look specifically at:

  • Error and rejection branches in validators.
  • Encoding and escaping functions.
  • Authentication and authorization checks.

Generate a report with phpunit --coverage-html build/coverage and read the uncovered lines in those files first. Coverage is a map of your blind spots, and the blind spots are where security bugs hide.

FAQ

What does "testar PHP" mean in a security context?

It means testing PHP code not just for correct behavior but for safe behavior: proving that untrusted input is validated, escaped, and parameterized, and that dependencies are free of known vulnerabilities. Functional tests answer "does it work?"; security tests answer "does it fail safely when abused?"

Is PHPUnit enough on its own to testar codigo PHP for security?

No. PHPUnit is excellent for asserting behavior on paths you write, but it cannot find dangerous patterns in code you never call in a test. Combine it with static analysis (Psalm or PHPStan, ideally with taint tracking) and dependency scanning with composer audit or an SCA tool.

How do I test for SQL injection in PHP?

Write a test that passes a classic injection string like ' OR '1'='1 into your data-access layer and assert it returns no unintended rows. Then confirm at the code level that all queries use PDO prepared statements with bound parameters rather than string concatenation.

Should security tests run on every commit or nightly?

Fast checks (unit tests, composer audit) belong on every push and pull request so problems are caught before merge. Slower or noisier scans can run nightly, but the goal is to shift as much as possible into the per-commit pipeline where feedback is immediate.

Never miss an update

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