Programming Beginner 8 min

How to Prevent SQL Injection in PHP

SQL injection is still one of the most exploited vulnerabilities on the web, and it is entirely preventable. It happens when user-supplied input is concatenated directly into a SQL query — giving an attacker the ability to read, modify, or delete any data in your database. This guide shows the right approach for every situation.

Step-by-step

  1. 1

    Understand the vulnerability first

    The classic injection happens when string concatenation builds the query:

    php
    // VULNERABLE — never do this
    $email = $_POST['email'];
    $result = mysqli_query($conn, "SELECT * FROM users WHERE email = '$email'");
    
    // An attacker submits: ' OR '1'='1
    // The query becomes:
    // SELECT * FROM users WHERE email = '' OR '1'='1'
    // This returns every row in the table.
  2. 2

    Use PDO prepared statements

    Prepared statements separate the query structure from the data. The database driver handles escaping internally — the data can never be interpreted as SQL syntax.

    php
    // Connect with PDO
    $pdo = new PDO(
        'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
        'db_user',
        'db_password',
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    
    // Safe: positional placeholder
    $stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
    $stmt->execute([$email]);
    $user = $stmt->fetch(PDO::FETCH_ASSOC);
  3. 3

    Use named placeholders for readability

    Named placeholders are easier to read and maintain when a query has multiple parameters, and they avoid ordering mistakes.

    php
    $stmt = $pdo->prepare(
        'INSERT INTO users (name, email, role) VALUES (:name, :email, :role)'
    );
    $stmt->execute([
        ':name'  => $name,
        ':email' => $email,
        ':role'  => $role,
    ]);
  4. 4

    Stop using escape functions — they are the wrong abstraction

    mysql_real_escape_string (removed in PHP 7), addslashes, and mysqli_real_escape_string are escape functions, not parameterization. They can be bypassed with certain character encodings, and they do not protect against injection in numeric contexts where quotes are not used. Prepared statements are not "the same thing but more work" — they are a fundamentally different mechanism that removes the attack surface entirely.

    php
    // STILL WRONG — do not do this
    $safeEmail = mysqli_real_escape_string($conn, $_POST['email']);
    $result = mysqli_query($conn, "SELECT * FROM users WHERE email = '$safeEmail'");
    
    // Also wrong for numeric columns — no quotes, so escaping does nothing
    $id = mysqli_real_escape_string($conn, $_GET['id']);
    $result = mysqli_query($conn, "SELECT * FROM users WHERE id = $id");
    // Attacker submits: 1 UNION SELECT password FROM admins
  5. 5

    Allow-list dynamic identifiers

    Table names, column names, and sort directions cannot be passed as PDO parameters — the driver will quote them as string literals, breaking the query. Instead, validate them against an explicit allow-list before interpolating.

    php
    // Dynamic ORDER BY column chosen by the user
    $allowedColumns = ['name', 'created_at', 'email'];
    $column = in_array($_GET['sort'] ?? '', $allowedColumns, true)
        ? $_GET['sort']
        : 'created_at'; // safe default
    
    $allowedDirections = ['ASC', 'DESC'];
    $direction = in_array(strtoupper($_GET['dir'] ?? ''), $allowedDirections, true)
        ? strtoupper($_GET['dir'])
        : 'DESC';
    
    // Safe to interpolate because only known-good values reach here
    $stmt = $pdo->prepare("SELECT * FROM users ORDER BY {$column} {$direction}");
    $stmt->execute();
  6. 6

    Use Laravel query builder or Eloquent safely

    Laravel's query builder and Eloquent use PDO prepared statements internally for all standard methods. You get injection protection by default.

    php
    // Safe — query builder parameterizes automatically
    $user = DB::table('users')->where('email', $email)->first();
    
    // Safe — Eloquent does the same
    $user = User::where('email', $email)->first();
    
    // Also safe — explicit binding on a raw fragment
    $users = DB::table('users')
        ->whereRaw('email = ? AND active = ?', [$email, 1])
        ->get();
  7. 7

    Identify and fix unsafe raw queries in Laravel

    DB::raw, selectRaw, whereRaw, and orderByRaw accept arbitrary SQL strings. They are safe when you pass user input as a binding — they re-open the injection hole when you concatenate.

    php
    // VULNERABLE — string concatenation inside a raw call
    $users = DB::select("SELECT * FROM users WHERE name = '{$name}'");
    
    // SAFE — pass input as a binding, never inside the string
    $users = DB::select('SELECT * FROM users WHERE name = ?', [$name]);
    
    // VULNERABLE — raw with concatenated input
    $users = DB::table('users')
        ->whereRaw("name = '{$name}'")->get();
    
    // SAFE — raw with binding placeholder
    $users = DB::table('users')
        ->whereRaw('name = ?', [$name])->get();

Tips & gotchas

  • Set <code>PDO::ATTR_EMULATE_PREPARES => false</code> when connecting. This forces real prepared statements at the driver level rather than client-side emulation, which is more secure.
  • Principle of least privilege applies to database users too — your application's DB user should only have <code>SELECT, INSERT, UPDATE, DELETE</code> on its own tables, never <code>DROP</code> or <code>CREATE</code>.
  • Input validation is a complement to parameterization, not a substitute. Validate types and formats to catch bugs early, but always parameterize regardless.
  • Automated scanners like SQLMap will find injection vulnerabilities quickly. Run it against your staging environment to verify your fixes.

Wrapping up

SQL injection prevention boils down to one rule: never put user input directly into a SQL string. Use PDO prepared statements with positional or named placeholders, allow-list any identifiers that must be dynamic, and treat all ORM raw-query helpers with the same caution as hand-written SQL. Follow these rules consistently and SQL injection becomes a non-issue.

#PHP #Security #Database
Back to all guides

Need Help With Your Project?

Book a free 30-minute consultation to discuss your technical challenges and explore solutions together.