Step-by-step
-
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
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
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
Stop using escape functions — they are the wrong abstraction
mysql_real_escape_string(removed in PHP 7),addslashes, andmysqli_real_escape_stringare 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
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
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
Identify and fix unsafe raw queries in Laravel
DB::raw,selectRaw,whereRaw, andorderByRawaccept 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.