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.
login.php (vulnerable)
<?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
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
$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
$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.