Security & Compliance

5 min read

SQL Injection in PHP: Vulnerable Code vs. Safe Code

SQL injection in PHP remains one of the most common and damaging web vulnerabilities. It is also one of the easiest to prevent. A single line of unsafe…

PHP-SQL-injection-Vulnerable-Code-vs-Safe-code-wimira

SQL injection in PHP remains one of the most common and damaging web vulnerabilities. It is also one of the easiest to prevent. A single line of unsafe code can expose your entire database to an attacker. This guide shows how the attack works, compares vulnerable and safe PHP code side by side, explains what attackers can steal, and covers how to test your own application safely.

How SQL Injection in PHP Works

SQL injection happens when user input is placed directly into a database query as part of the SQL itself. The database cannot tell the difference between your intended query and the extra commands an attacker slipped in through a form field, URL parameter or cookie.

Imagine a login form. If your code builds the query by joining strings, an attacker can type SQL syntax instead of a normal email address. The database then runs that syntax as part of the query. The root problem is mixing code and data. The fix is to keep them separate.

Vulnerable PHP Code Example

This is a classic unsafe login query:

php
$email = $_POST['email'];
$password = $_POST['password'];

$sql = "SELECT * FROM users
        WHERE email = '$email' AND password = '$password'";
$result = $pdo->query($sql);

If an attacker enters ' OR '1'='1 as the email, the condition becomes true for every row. The query can return a user record without a valid password. Even worse, the same weakness can be used to read other tables or change data.

The code fails because $email and $password go straight into the SQL string with no separation between command and value.

Safe PHP Code: Prepared Statements with PDO

Prepared statements, also called parameterized queries, solve the problem. The SQL structure is sent to the database first. The values are sent separately, so they can never change the structure of the query.

php
$stmt = $pdo->prepare(
    'SELECT id, password_hash FROM users WHERE email = :email'
);
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if ($user && password_verify($_POST['password'], $user['password_hash'])) {
    // login successful
}

Three improvements stand out. The email is bound as a parameter, so it is treated purely as data. Passwords are stored as hashes and checked with password_verify(), not compared in SQL. Only the needed columns are selected. For extra safety, set PDO::ATTR_EMULATE_PREPARES to false so the database performs real preparation.

What Attackers Can Steal

A successful injection can do far more than bypass a login. Depending on database permissions, an attacker may be able to:

  • Read customer names, emails, phone numbers and addresses
  • Extract password hashes and attempt to crack them offline
  • Dump order history, invoices and other business records
  • Modify or delete data, including whole tables
  • Create admin accounts to keep access to your site
  • In some setups, read files or reach other systems through the database server

For a business, the damage goes beyond the stolen data. It includes downtime, legal exposure, loss of customer trust and the cost of incident response. A breach caused by a known, preventable flaw is especially hard to explain.

How to Test for SQL Injection Safely

Test only systems you own or have written permission to test. Use a local or staging copy, never your live production database.

  1. Review the code. Search for queries built with string concatenation or variables inside SQL strings. Every match is a candidate for a fix.
  2. Run automated scanners. Tools such as OWASP ZAP and sqlmap can probe your staging site for injectable parameters.
  3. Try harmless test input. Enter a single quote in form fields and watch for database errors or odd behavior on staging.
  4. Check error handling. Turn off detailed database errors in production so attackers learn nothing from failed queries.
  5. Add static analysis. Tools like PHPStan and Psalm can catch risky patterns during development.

Keep backups before testing and document every finding so it can be fixed and re-tested.

ORM and Framework Notes: Laravel and Symfony

Frameworks help, but they do not make you immune.

Laravel. Eloquent and the query builder use parameter binding by default, so User::where('email', $email)->first() is safe. Risk appears when you use raw expressions. Avoid concatenating input into DB::raw(), whereRaw() or orderByRaw(). If you must use raw SQL, pass bindings, for example whereRaw('email = ?', [$email]).

Symfony and Doctrine. Doctrine’s query builder and DQL support named parameters through setParameter(). Never build DQL or native SQL by adding user input to the string. Column names and sort directions cannot be bound as parameters, so validate them against a fixed allow list.

In every framework, the rule is the same: user input should only ever travel as a bound parameter or an allow listed value.

Conclusion: Make Prepared Statements Your Default

SQL injection in PHP is a code-level mistake with a simple, reliable fix. Use prepared statements everywhere, hash passwords, limit database permissions, hide detailed errors and test regularly on staging. Whether you write plain PDO or use Laravel or Symfony, keeping data separate from SQL is what protects your users and your business.