Question

How to Prevent SQL Injection in PHP

phpmysqlsqlsecuritysql-injection

Question

If user input is inserted directly into an SQL query without being handled safely, the application becomes vulnerable to SQL injection.

For example:

$unsafe_variable = $_POST['user_input'];

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

In this case, a malicious user could submit input such as:

value'); DROP TABLE table;--

That could turn the query into something like:

INSERT INTO `table` (`column`) VALUES ('value'); DROP TABLE table;--')

How can this be prevented in PHP?

Short Answer

By the end of this page, you will understand what SQL injection is, why directly concatenating user input into SQL is dangerous, and how to prevent it in PHP using prepared statements, parameterized queries, and basic validation. You will also see common mistakes, real-world usage patterns, and a small practical project.

Concept

SQL injection happens when untrusted input is treated as part of an SQL command instead of as plain data.

In unsafe code, a query is built by joining strings:

$sql = "INSERT INTO users (name) VALUES ('" . $_POST['name'] . "')";

If the input contains SQL syntax such as quotes, comments, or extra commands, the database may interpret that input as executable SQL.

The core protection is prepared statements.

With prepared statements, you send the SQL structure separately from the data. The database knows which parts are commands and which parts are values. That means user input is handled as data only, not as SQL code.

Safe code looks like this:

$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->execute([$_POST['name']]);

Why this matters in real programming:

  • It protects login forms, search forms, filters, and admin tools.
  • It prevents attackers from reading, changing, or deleting data.
  • It avoids fragile string-escaping logic.
  • It is the standard secure way to interact with databases in PHP.

Important note: the old mysql_* functions are deprecated and removed. In modern PHP, use PDO or MySQLi.

Mental Model

Think of SQL as a form with blanks to fill in.

  • The SQL query is the printed form.
  • User input is the value written into the blanks.

If you build SQL by concatenating strings, you are letting the user write directly on the form, including in the instruction area. They can change the meaning of the whole form.

Prepared statements work like locked fields:

  • The structure of the form is fixed first.
  • The user can only provide values for specific blanks.
  • Their input cannot rewrite the instructions.

So the rule is simple: users provide data, not SQL syntax.

Take Quiz

Syntax and Examples

Safe syntax with PDO

$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'username', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (:name)");
$stmt->execute([
    ':name' => $_POST['user_input']
]);

Why this is safe

  • :name is a placeholder.
  • The SQL statement is prepared first.
  • The value from $_POST['user_input'] is sent separately.
  • Even if the input contains quotes or SQL-like text, it is treated as data.

Safe syntax with MySQLi

$mysqli = new mysqli('localhost', 'username', 'password', 'test');

$stmt = $mysqli->prepare("INSERT INTO users (name) VALUES (?)");
$stmt->bind_param('s', $_POST['user_input']);
$stmt->execute();

Meaning of 's'

In bind_param('s', ...), the s means the value is a string.

Common types:

  • s = string
  • i = integer
  • d = double
  • b = blob

Example with a SELECT query

Unsafe:

$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";

Safe with PDO:

$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $_GET['id']]);
$user = $stmt->fetch();

Input validation still matters

Prepared statements prevent injection, but validation is still useful.

For example, if id must be an integer:

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if ($id === false) {
    exit('Invalid id');
}

$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);

Validation checks whether the data is acceptable for your business rules. Prepared statements make the SQL safe.

Step by Step Execution

Consider this PDO example:

$name = "value'); DROP TABLE users;--";

$stmt = $pdo->prepare("INSERT INTO users (name) VALUES (:name)");
$stmt->execute([':name' => $name]);

Step by step

  1. $name is assigned a suspicious-looking string.

  2. $pdo->prepare(...) sends the SQL structure to the database:

    INSERT INTO users (name) VALUES (:name)
    
  3. The database sees :name as a value placeholder, not as part of the SQL command.

  4. $stmt->execute([':name' => $name]) sends the input separately.

  5. The database stores the entire text:

    value'); DROP TABLE users;--
    
  6. No extra SQL command is executed.

Compare with unsafe concatenation

$name = "value'); DROP TABLE users;--";
$sql = "INSERT INTO users (name) VALUES ('$name')";

Now the input is mixed directly into the SQL string. The database may interpret parts of it as SQL syntax. That is the vulnerability.

Key takeaway

The same input can be either:

  • harmless data with prepared statements
  • dangerous SQL when concatenated into a query string

Real World Use Cases

Prepared statements are used anywhere an application sends external input to a database.

Common examples

  • Login forms: checking email and password records safely
  • Registration forms: inserting usernames, emails, and profile data
  • Search filters: querying products, users, or orders by user-provided terms
  • Admin dashboards: updating records based on form submissions
  • REST APIs: reading route parameters and request body values safely
  • Import scripts: inserting CSV or JSON data into tables

Example: login lookup

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

Example: product search

$stmt = $pdo->prepare("SELECT * FROM products WHERE category_id = :category_id");
$stmt->execute([':category_id' => $_GET['category_id']]);
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

Example: updating a row

$stmt = $pdo->prepare("UPDATE users SET display_name = :name WHERE id = :id");
$stmt->execute([
    ':name' => $_POST['display_name'],
    ':id' => $_POST['id']
]);

In all of these cases, the pattern is the same: never merge raw input into SQL strings.

Take Quiz

Real Codebase Usage

In real PHP projects, developers usually combine prepared statements with a few other habits.

1. Validation before querying

Check that the input matches expectations.

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
    throw new InvalidArgumentException('Invalid user id');
}

2. Guard clauses

Stop early when required input is missing or invalid.

$email = trim($_POST['email'] ?? '');
if ($email === '') {
    exit('Email is required');
}

3. Centralized database access

Many projects create one PDO connection and reuse it through repository or service classes.

function findUserByEmail(PDO $pdo, string $email): array|false {
    $stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
    $stmt->execute([':email' => $email]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

4. Error handling with exceptions

$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

This makes database problems easier to catch and debug safely.

5. Whitelisting for SQL identifiers

Prepared statements protect values, but not table names or column names. If a column name is dynamic, developers usually whitelist allowed values.

$allowedSortColumns = ['name', 'created_at'];
$sort = $_GET['sort'] ?? 'name';

if (!in_array($sort, $allowedSortColumns, true)) {
    $sort = 'name';
}

$sql = "SELECT * FROM users ORDER BY $sort";

Here, whitelisting is needed because placeholders cannot usually replace identifiers like column names.

6. Secure authentication flow

For passwords, real projects use password_hash() and password_verify() rather than trying to compare plain text in SQL.

Common Mistakes

1. Using string concatenation

Broken:

$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";

Why it is wrong:

  • Raw input is inserted directly into the query.
  • Quotes inside input can break the SQL structure.

Fix:

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $_POST['email']]);

2. Relying only on escaping

Broken idea:

$email = addslashes($_POST['email']);
$sql = "SELECT * FROM users WHERE email = '$email'";

Why it is wrong:

  • addslashes() is not a safe SQL injection defense.
  • Escaping rules depend on the database connection and encoding.

Fix:

  • Use prepared statements.

3. Using removed mysql_* functions

Broken:

mysql_query($sql);

Why it is wrong:

  • mysql_* is obsolete and removed from modern PHP.
  • It encourages outdated coding patterns.

Fix:

  • Use PDO or MySQLi.

4. Assuming numeric input is always safe

Broken:

$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = $id";

Why it is risky:

  • Even expected numeric input should not be trusted blindly.
  • Validation and parameter binding should still be used.

Fix:

$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false) {
    exit('Invalid id');
}

$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id");
$stmt->execute([':id' => $id]);

5. Trying to bind table names or column names

Broken idea:

$stmt = $pdo->prepare("SELECT * FROM users ORDER BY :column");

Why it is wrong:

  • Placeholders are for values, not SQL keywords, table names, or column names.

Fix:

  • Use a whitelist of allowed identifiers.

6. Forgetting business-rule validation

Prepared statements stop SQL injection, but they do not ensure the value is meaningful.

Example:

  • An age of -500
  • An empty email
  • A username that is too long

You still need validation for correctness.

Comparisons

ApproachSafe from SQL injection?Recommended?Notes
String concatenationNoNoUser input becomes part of SQL
addslashes()NoNoNot a reliable SQL defense
mysqli_real_escape_string()Better but not idealUsually no for new codeSafer than manual escaping, but prepared statements are better
Prepared statements with PDOYesYesClean, flexible, widely used
Prepared statements with MySQLiYesYesGood option for MySQL specifically

PDO vs MySQLi

FeaturePDOMySQLi
Supports multiple databasesYesNo, MySQL only
Prepared statementsYesYes
Named placeholdersYesNo
Positional placeholdersYesYes
Common beginner choiceYesYes

Validation vs prepared statements

PurposeValidationPrepared statements
Checks if input is acceptableYesNo
Prevents SQL injectionNoYes
ExampleFILTER_VALIDATE_INTprepare() + execute()

You usually need both: validation for correctness and prepared statements for security.

Take Quiz

Cheat Sheet

Safe rule

Never put raw user input directly into SQL strings.

Use PDO

$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute([':email' => $email]);

Use MySQLi

$stmt = $mysqli->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param('s', $email);
$stmt->execute();

Good practices

  • Use prepared statements for all external input
  • Use PDO or MySQLi, not mysql_*
  • Validate input with filter_input() or custom checks
  • Use whitelists for dynamic column names or sort directions
  • Enable PDO exceptions for easier debugging

Important edge cases

  • Placeholders work for values, not table names or column names
  • Prepared statements do not replace business validation
  • Escaping functions are not the preferred modern solution

Unsafe example

$sql = "SELECT * FROM users WHERE name = '" . $_POST['name'] . "'";

Safe example

$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name");
$stmt->execute([':name' => $_POST['name']]);

FAQ

Is addslashes() enough to prevent SQL injection?

No. It is not a reliable SQL injection defense. Use prepared statements instead.

Should I still validate input if I use prepared statements?

Yes. Prepared statements make queries safe, but validation checks whether the input is acceptable for your application.

Is PDO better than MySQLi?

Both support prepared statements and are safe when used correctly. PDO is more flexible because it supports multiple database systems.

Can prepared statements protect table names and column names?

No. Placeholders are for values only. For dynamic identifiers, use a whitelist.

Are old mysql_query() functions safe to use?

No. The mysql_* extension is obsolete and removed. Use PDO or MySQLi.

Do prepared statements stop all security problems?

No. They prevent SQL injection when used properly, but you still need validation, authentication, authorization, and proper error handling.

What about numeric values like IDs?

They should still be validated and bound as parameters. Do not trust request data just because it looks numeric.

Take Quiz

Mini Project

Description

Build a small PHP form that saves a comment into a MySQL database safely. This demonstrates the most common real-world fix for SQL injection: using PDO prepared statements instead of string concatenation.

Goal

Create a comment submission script that accepts user input, validates it, and inserts it into the database without exposing the application to SQL injection.

Requirements

  • Create a PDO database connection with error mode enabled
  • Read a comment from a POST request
  • Reject empty comments
  • Insert the comment using a prepared statement
  • Show a success message when the insert works
Take Quiz

Keep learning

Related questions

View more