PHPPreventing SQL Injection

Preventing SQL Injection

SQL injection happens when user-supplied text is pasted directly into a database query as if it were trusted code, instead of being treated as pure data. A query is a small program written in SQL, and string concatenation lets an attacker rewrite parts of that program just by controlling one input field. It remains one of the most damaging vulnerabilities in web applications because a successful injection can read, modify, or delete an entire database in a single request.

Vulnerable — string concatenation into a query

Here is a login check that looks harmless at a glance: it takes a username and password from a form and checks whether a matching row exists.

Vulnerable — do not use
This code builds SQL by concatenating raw request data into the query string. It is shown only so the exploit below makes sense — never write database code this way.

login.php (vulnerable)

PHP
<?php
$username = $_POST['username'];
$password = $_POST['password'];

$sql = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);

if (mysqli_num_rows($result) === 1) {
    // "logged in"
}

An attacker does not need to guess a real password. If they submit admin as the username and anything' OR '1'='1 as the password, the query built by the code above becomes:

Resulting query after substitution

HTML
SELECT * FROM users WHERE username = 'admin' AND password = 'anything' OR '1'='1'

Because SQL evaluates AND before OR, this reads as "match the password check, or just always be true." The final clause is always true, so the whole WHERE condition is true for every row in the table, and the query returns every user — including the admin account — without the attacker ever knowing a real password. A slightly more aggressive payload could use a stacked UNION SELECT to pull data out of a completely different table, or a semicolon to chain a second destructive statement, depending on what the database driver allows.

The fix: prepared statements

A prepared statement sends the SQL structure and the user-supplied values to the database as two separate things. The database compiles the query template first, with placeholders standing in for the values, and only afterward binds the actual data into those placeholders. Because the data is never parsed as part of the SQL text, there is no way for it to change the query's structure — quotes, semicolons, and SQL keywords typed into a form field are just inert characters as far as the query is concerned.

login.php (safe — PDO prepared statement)

PHP
<?php
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', $dbUser, $dbPass, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$username = $_POST['username'];
$password = $_POST['password'];

$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch();

if ($user && password_verify($password, $user['password_hash'])) {
    // genuinely logged in
}

Notice that the password is no longer part of the query at all — the correct pattern for authentication is to fetch the row by username only, then verify the submitted password against a stored hash with password_verify(), which is covered on its own page. For queries that need multiple bound values, named placeholders make the code easier to read.

Bound parameters with mysqli

PHP
<?php
$stmt = $conn->prepare('SELECT * FROM orders WHERE customer_id = ? AND status = ?');
$stmt->bind_param('is', $customerId, $status);
$stmt->execute();
$result = $stmt->get_result();
Why input validation alone is not enough

It is tempting to think that checking a field's format — "usernames can only be letters and numbers," "this field must be numeric" — is sufficient protection. Validation is a useful first layer, and it does stop some attacks, but it is not a substitute for prepared statements for two reasons. First, many legitimate inputs cannot be restricted to a safe character set — a search box, a comment field, or a person's name (like O'Brien) can contain an apostrophe legitimately, so you cannot simply reject every quote character. Second, validation logic has to be applied correctly on every single input path with no exceptions, forever, while a prepared statement makes the entire class of attack structurally impossible regardless of what the value contains. Relying on validation as your only defense means one missed field, one new form, or one refactor away from a vulnerability.

Escaping functions are a weak fallback

Older PHP code sometimes uses mysqli_real_escape_string() to "sanitize" a value before concatenating it into a query string. This function escapes characters like quotes so they cannot terminate a string literal early, and it is better than nothing, but it is still fragile: it has to be called correctly on every single value, on every single query, with no exceptions, and its safety can depend on connection character set settings. Prepared statements remove the need to get this right by hand for every query — the separation between code and data is enforced by the database driver itself, not by a function call a developer has to remember to add.

# Vulnerable query, attacker submits password: anything' OR '1'='1
SELECT * FROM users WHERE username = 'admin' AND password = 'anything' OR '1'='1'
-- returns every row in the users table
  • Never concatenate user input directly into a SQL string, in any language or framework.

  • Use prepared statements (PDO or mysqli) with bound parameters for every query that includes a variable.

  • Verify passwords with password_verify() against a stored hash — never compare passwords inside a SQL WHERE clause.

  • Treat validation and escaping as extra layers on top of prepared statements, not replacements for them.

ORMs are not automatically immune
Query builders and ORMs (Eloquent, Doctrine) use prepared statements under the hood for their normal query methods, which is why they are generally safe. But most of them also expose a raw or "unsafe" query method for edge cases — if you ever drop down to a raw SQL string inside an ORM, the same concatenation rules and risks apply exactly as they do in plain PHP.
Tip
If you are auditing existing code for this vulnerability, search the codebase for SQL keywords like SELECT, INSERT, UPDATE, and DELETE appearing inside double-quoted strings next to a variable — that pattern is the fastest way to spot concatenated queries that need to be rewritten as prepared statements.