PHP Code Security Audit: The Checklist for Legacy and AI-Generated Codebases

#php security audit
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Founder & Lead Developer

Expert in software development and legacy code optimization

A PHP security audit is not a single scan you run before launch. It is a structured review across multiple layers of a codebase - authentication design, data flow, dependency hygiene, secrets handling, and runtime configuration - that surfaces the gaps static analysis tools alone will miss.

This post explains what a thorough PHP security audit covers, what the written output looks like, how AI-generated code has changed the threat surface compared to older legacy systems, and how to prepare your codebase before commissioning one.

Why PHP Codebases Need a Dedicated Security Audit

PHP has an unusually long tail. Applications started on PHP 5.3 are still running in production. Frameworks like Symfony, Laravel, and custom stacks each carry their own security primitives, and a vulnerability pattern that is idiomatic in one is catastrophic in another.

At the same time, the rise of AI-assisted development has introduced a new class of issues. LLM-generated PHP tends to be functional on the surface but inconsistent underneath: correct query parameterisation in one file, raw string concatenation in the file it generated 30 seconds later. The patterns differ from legacy bugs but the blast radius is the same.

A php code audit addresses both: the technical debt accumulated over years of human development and the inconsistency introduced by code that was generated rather than reviewed.

The Seven Dimensions of a PHP Security Audit

1. Authentication and Authorisation

Authentication failures are OWASP's number one web application risk category. In PHP the most common problems are not exotic - they are structural.

What auditors check:

  • Password hashing: is password_hash() with PASSWORD_BCRYPT or PASSWORD_ARGON2ID used consistently, or are there legacy md5()/sha1() hashes still in the codebase?
  • Session fixation: is session_regenerate_id(true) called after login?
  • Authorisation checks: are they enforced at the controller layer or are they scattered inconsistently, leaving some routes unprotected?
  • Multi-factor: where it exists, is the MFA check enforced server-side or only on the frontend?
  • Remember-me tokens: are they stored as secure hashes rather than cleartext session IDs?

In Symfony specifically, auditors look at the security.yaml firewall ordering. A misconfigured firewall can expose API routes that should be protected, because Symfony matches the first firewall pattern that fits the request path. An endpoint added after a ^/api pattern that was intended to be open can inadvertently inherit that openness if the order is wrong.

2. Input Handling and SQL Exposure

Injection remains the most exploited vulnerability class in PHP applications that were not built on a modern ORM from the start.

What auditors check:

  • Raw SQL: every instance of string concatenation or interpolation inside a query is flagged, regardless of whether the variable looks safe.
  • Prepared statements and parameterised queries: auditors verify these are used consistently, not just on the routes that handle user-visible forms.
  • ORM misuse: Doctrine allows raw DQL with interpolated strings. Auditors check QueryBuilder calls for ->where("u.email = '$email'") patterns.
  • Indirect injection surfaces: search filters, sort parameters, and export features that accept column names are common blind spots.
  • XML and LDAP injection: less common but present in enterprise PHP codebases that integrate with directory services or parse external XML feeds.

3. Secrets Management

Credentials embedded in source code are a perennial PHP problem. The typical lifecycle is: developer hardcodes a database password during a quick fix, commits it, the secret stays in git history for years, the company rotates the password but nobody checks whether the old value is still checked into a branch somewhere.

What auditors check:

  • .env files committed to the repository, including historical commits.
  • API keys, database passwords, or private keys in configuration files, PHP constants, or class properties.
  • Logging statements that print request parameters, which can include tokens or passwords.
  • Cloud provider credential files (~/.aws/credentials, service account JSON) referenced from PHP configuration.
  • Secrets in Symfony parameters.yaml or config/services.yaml that are committed without environment variable substitution.

The audit includes a historical git scan - not just the current HEAD - because secrets removed from HEAD but present in prior commits are fully recoverable by anyone with repository access.

4. Dependency and CVE Scanning

A modern PHP application typically has 80-200 Composer dependencies. Each is a potential entry point.

What auditors check:

  • composer audit output and any unresolved CVEs in direct or transitive dependencies.
  • PHP version: whether the runtime version in use still receives security patches. PHP 8.0 reached end-of-life in November 2023; 8.1 reaches it in December 2025.
  • Dependencies pinned at major versions that have known critical vulnerabilities in the locked minor version.
  • Abandoned packages - Packagist flags these - that receive no security patches regardless of CVE status.
  • The gap between composer.json and composer.lock: if composer.lock is not committed, there is no reproducible dependency state and no reliable baseline for the audit.

5. Session Handling

PHP session configuration is spread across php.ini, session_start() options, and application code. The defaults are rarely appropriate for a production security posture.

What auditors check:

  • session.cookie_secure: must be true on any application served over HTTPS.
  • session.cookie_httponly: prevents client-side JavaScript from accessing the session cookie.
  • session.cookie_samesite: Lax or Strict mitigates CSRF via cookie theft.
  • Session lifetime: whether sessions expire after inactivity or only on explicit logout.
  • Session storage: file-based sessions on shared hosting can be read by other tenants. Database or Redis-backed session storage is audited for access controls.
  • Session data integrity: applications that store sensitive flags (e.g. $_SESSION['is_admin'] = true) without server-side verification against a database record are flagged.

6. File I/O and Permission Errors

File-related vulnerabilities are underestimated in PHP because they often require chaining with another weakness to exploit. But they appear frequently in codebases with upload features, report generation, or dynamic include patterns.

What auditors check:

  • Path traversal: user-controlled input fed into file_get_contents(), fopen(), include(), or require() without normalisation.
  • Dynamic includes: include $_GET['page'] . '.php' is textbook remote/local file inclusion. Auditors flag any dynamic include, even when the developer has added a partial guard.
  • File upload handling: MIME type validation done only on the client-supplied Content-Type header (trivially spoofed) rather than server-side detection via finfo.
  • Upload storage: files uploaded to a directory inside the web root that are directly accessible by URL, bypassing application logic.
  • Temporary file handling: use of tmpfile() vs. insecure patterns that create predictable filenames in shared /tmp directories.

7. Symfony Firewall Ordering and Framework-Specific Risks

Symfony applications have a class of configuration bugs that are invisible to generic scanners.

Firewall ordering: Symfony evaluates firewalls top-to-bottom and stops at the first match. A route intended to be public that is defined before the authenticated firewall pattern will be unprotected. A route intended to be protected that is defined after an overly broad pattern may inherit incorrect access rules. The audit traces every route to its effective firewall and verifies the access_control list matches the intended authorisation model.

Security voters and role hierarchy: Custom voters that implement supportsAttribute() incorrectly can silently abstain from access decisions, defaulting to the configured access decision strategy. In unanimous mode this is secure; in affirmative (the default) an abstaining voter lets other voters grant access.

CSRF token validation: Symfony's form component includes CSRF protection by default, but it can be disabled per-form. Auditors check for csrf_protection: false in form types and for AJAX endpoints that bypass form handling entirely without their own token validation.

Event listener priority conflicts: Security listeners registered at the wrong priority can be bypassed by other listeners that execute first and short-circuit the request cycle.

How AI-Generated Code Changes the Threat Surface

Legacy PHP codebases accumulate security debt incrementally. A developer writes an insecure pattern, it gets copied, the team learns and stops using it, but the old files remain. The threat surface is concentrated in the older parts of the codebase and tends to cluster around the same patterns.

AI-generated PHP is different in two ways.

Inconsistency within a single session. An LLM generates a Doctrine entity with properly parameterised queries, then generates a repository class in the same session that builds a filter query with string interpolation. Both look correct at a glance. The inconsistency is not because the AI does not know the right pattern - it does - but because it generates code statistically, not architecturally. Security audits of AI-assisted codebases find more variance per 1,000 lines than audits of purely human-written code.

Plausible but wrong security code. AI-generated authentication code often passes a surface review: it calls password_verify(), it uses session_regenerate_id(), it checks a role before returning data. But the generated code frequently misses context: the role check is performed before the session is authenticated, or session_regenerate_id() is called without true (leaving the old session active), or password_verify() is present but the comparison result is not actually used to gate access. These bugs are harder to catch than raw SQL concatenation because they look structurally correct.

Missing defence-in-depth. Legacy code at least usually has the framework's built-in protections even if application-level code is weak. AI-generated code sometimes omits framework conventions entirely - bypassing Symfony form handling, generating custom session wrappers, or rolling its own CSRF validation - creating gaps that the framework would otherwise close automatically.

What the Written Output of a PHP Security Audit Looks Like

A professional security audit delivers more than a scanner report. The written output typically includes:

  • An executive summary covering the overall risk posture, the most critical findings, and the recommended remediation priority order.
  • A findings register with each issue classified by severity (Critical, High, Medium, Low), the affected file and line range, a description of the vulnerability, a proof-of-concept or reproduction path, and a concrete remediation recommendation.
  • A secure code reference showing the insecure pattern alongside the corrected pattern for the most common issue types found in that specific codebase.
  • A configuration review covering php.ini settings, web server configuration, and framework security configuration.
  • A dependency CVE table listing affected packages, the CVE identifiers, the patched version, and the exploitability context (not all CVEs are exploitable in every application).
  • Remediation effort estimates that allow engineering teams to plan sprint capacity.

The findings register is the most actionable part. A good audit does not just list vulnerabilities - it gives the development team a clear path from current state to a defensible baseline.

How to Prepare Your Codebase Before Commissioning an Audit

A php code auditing engagement produces better results when the codebase is prepared. The following steps reduce the time spent on low-signal findings and focus the audit on real risk.

Run composer audit first. Resolve or document all known CVEs in dependencies before the audit begins. An audit that spends significant time on already-known issues is not a good use of budget.

Commit composer.lock. If it is not in the repository, add it. The audit needs a reproducible dependency state.

Enable strict types. Adding declare(strict_types=1) to files that lack it reduces the class of type coercion bugs that auditors need to trace manually. It also makes the codebase easier to reason about during review.

Consolidate environment configuration. Ensure all secrets are loaded from environment variables or a vault, not from committed files. Run a git history scan (tools like git-secrets or trufflehog) before the audit to identify and rotate any secrets in the commit history.

Document authentication boundaries. A one-page map of which routes require authentication, which roles exist, and what each role can access saves hours of reverse-engineering during the audit and produces a clearer findings register.

Run your test suite. Auditors verify remediations against existing test coverage. A codebase with no tests requires the remediation phase to include test writing, which extends the engagement timeline.

Self-Assessment Checklist

Before commissioning a full audit, use this checklist to identify the most obvious gaps. Any "no" answer is a finding worth addressing immediately.

AreaCheck
AuthenticationPasswords hashed with password_hash() + BCRYPT or ARGON2ID
Authenticationsession_regenerate_id(true) called on login
AuthenticationAuthorisation checks enforced server-side on every protected route
SQLAll queries use prepared statements or ORM with no raw interpolation
SQLSearch, filter, and sort parameters validated against an allowlist
SecretsNo credentials in committed files or git history
SecretsAll secrets loaded from environment variables
SecretsLogging does not print request parameters or headers
Dependenciescomposer.lock committed to the repository
Dependenciescomposer audit returns zero unresolved CVEs
DependenciesPHP version still receives security patches
Sessionssession.cookie_secure = On in production
Sessionssession.cookie_httponly = On
Sessionssession.cookie_samesite = Lax or Strict
File handlingNo user-controlled input in include/require paths
File handlingUploaded files stored outside the web root or served via a controller
SymfonyFirewall order reviewed against intended access model
SymfonyCSRF protection not disabled on sensitive form types
SymfonyCustom voters tested under unanimous and affirmative strategies

A "no" in the authentication, SQL, or secrets rows warrants immediate remediation rather than waiting for a formal audit.

Commissioning a PHP Security Audit

A PHP security audit is most valuable when it is scoped to a specific outcome: preparing for a compliance review, onboarding a new enterprise client that requires a security posture report, clearing a finding from a penetration test, or validating an AI-assisted codebase before its first production release.

Wolf-Tech conducts PHP and Symfony code audits as part of our code quality consulting practice. Engagements typically run one to three weeks depending on codebase size and the depth of the dependency graph. The output is the findings register and configuration review described above, delivered with remediation support.

If you are preparing for an audit or want to discuss what a review of your specific codebase would cover, write to hello@wolf-tech.io or visit wolf-tech.io.

FAQ

What is the difference between a PHP security audit and a penetration test?

A security audit is a code-level review: auditors read the source, trace data flows, and identify vulnerabilities from the inside. A penetration test is external: testers probe the running application from the outside without source access. Both are useful; a code audit typically finds more issues per hour of effort and produces more actionable remediation guidance.

How long does a PHP security audit take?

For a 50,000-100,000 line PHP/Symfony application with standard complexity, a thorough audit takes five to ten working days. Larger codebases, complex dependency graphs, or significant AI-generated content extend the timeline.

Does a security audit cover the database schema?

Yes, to the extent it is accessible during the engagement. Auditors review table structure for sensitive data storage patterns (unencrypted PII, cleartext credentials) and index design that might enable timing attacks.

What PHP version should I be running?

PHP 8.2 or 8.3 as of mid-2026. PHP 8.1 reaches end-of-life in December 2025 and will not receive further security patches. Running an unsupported PHP version means known vulnerabilities in the runtime itself will not be fixed.