Lesson 84 of 158 – SQL Injection Prevention
84%

SQL Injection Prevention

SQL injection is a security vulnerability that can occur when untrusted user input is directly added to an SQL query. In a REST API, attackers may try to manipulate API input to change the intended database query.

Note: The most important protection for SQL values in PHP PDO is to use prepared statements with bound parameters instead of directly concatenating user input into SQL.

1. What is SQL Injection?

SQL injection is an attack in which malicious input is used to interfere with the intended SQL statement.

Mobile App
     ↓
User Input
     ↓
REST API
     ↓
SQL Query
     ↓
Database

If the API builds SQL by directly concatenating untrusted input, the query may behave differently from what the developer intended.

2. Why SQL Injection is Dangerous

A vulnerable API may expose or modify data that the user should not be able to access.

Depending on the application and database permissions, SQL injection can potentially result in:

  • Unauthorized data access
  • Data modification
  • Data deletion
  • Authentication bypass attempts
  • Exposure of sensitive information

3. Vulnerable SQL Query

Consider the following PHP code:

$email = $_GET['email'];

$sql =
    "SELECT *
     FROM users
     WHERE email = '$email'";

$result =
    $pdo->query($sql);

The user input is directly inserted into the SQL statement. This is an unsafe pattern.

4. Never Concatenate User Input into SQL

Avoid constructing SQL statements by directly concatenating values received from users.

Unsafe:

$sql =
    "SELECT *
     FROM students
     WHERE student_id = '$studentId'";

The value should instead be supplied separately using a prepared statement.

5. What is a Prepared Statement?

A prepared statement separates the SQL structure from the values supplied by the user.

$stmt = $pdo->prepare(
    "SELECT *
     FROM students
     WHERE student_id = ?"
);

$stmt->execute([
    $studentId
]);

The user value is treated as data rather than being interpreted as part of the SQL structure.

6. PDO Prepared Statements

PDO provides prepared statements through prepare() and execute().

$stmt = $pdo->prepare(
    "SELECT *
     FROM users
     WHERE email = ?"
);

$stmt->execute([
    $email
]);

7. Positional Placeholders

A question mark can be used as a positional placeholder.

$sql = "
    SELECT *
    FROM students
    WHERE course = ?
    AND status = ?
";

$stmt = $pdo->prepare($sql);

$stmt->execute([
    $course,
    $status
]);

The values are supplied in the same order as the placeholders.

8. Named Placeholders

PDO also supports named placeholders.

$sql = "
    SELECT *
    FROM students
    WHERE course = :course
    AND status = :status
";

$stmt = $pdo->prepare($sql);

$stmt->execute([
    ':course' => $course,
    ':status' => $status
]);

Named placeholders can make queries easier to understand.

9. SQL Injection in Login APIs

Login APIs are especially important because they process credentials.

Unsafe:

$email =
    $_POST['email'];

$password =
    $_POST['password'];

$sql = "
    SELECT *
    FROM users
    WHERE email = '$email'
    AND password = '$password'
";

Credentials should never be directly inserted into SQL.

10. Secure Login Query

$stmt = $pdo->prepare(
    "SELECT *
     FROM users
     WHERE email = ?
     LIMIT 1"
);

$stmt->execute([
    $email
]);

$user =
    $stmt->fetch(
        PDO::FETCH_ASSOC
    );

The password should then be checked using password_verify() rather than being included directly in the SQL query.

11. Prepared Statements and UPDATE

Prepared statements should also be used when updating records.

$stmt = $pdo->prepare(
    "UPDATE students
     SET name = ?
     WHERE id = ?"
);

$stmt->execute([
    $name,
    $id
]);

12. Prepared Statements and DELETE

$stmt = $pdo->prepare(
    "DELETE FROM students
     WHERE id = ?"
);

$stmt->execute([
    $id
]);

The ID supplied by the client should still be validated before the operation is performed.

13. Prepared Statements and INSERT

$stmt = $pdo->prepare(
    "INSERT INTO students
     (name, email, course)
     VALUES (?, ?, ?)"
);

$stmt->execute([
    $name,
    $email,
    $course
]);

This separates the SQL statement from the supplied values.

14. Prepared Statements and Search

Search values should also be supplied as parameters.

$search =
    "%{$search}%";

$stmt = $pdo->prepare(
    "SELECT *
     FROM students
     WHERE name LIKE ?"
);

$stmt->execute([
    $search
]);

The wildcard characters are part of the value, not SQL syntax created from raw user input.

15. Multiple Search Parameters

$keyword =
    "%{$search}%";

$stmt = $pdo->prepare(
    "SELECT *
     FROM students
     WHERE name LIKE ?
     OR student_id LIKE ?"
);

$stmt->execute([
    $keyword,
    $keyword
]);

Both values are safely passed as parameters.

16. Complete Secure Student API

<?php

header(
    "Content-Type: application/json"
);

require_once '../db.php';

$search = trim(
    $_GET['search'] ?? ''
);

try {

    if ($search === '') {

        $stmt = $pdo->prepare(
            "SELECT
                id,
                student_id,
                name,
                course
             FROM students
             ORDER BY id DESC"
        );

        $stmt->execute();

    } else {

        $keyword =
            "%{$search}%";

        $stmt = $pdo->prepare(
            "SELECT
                id,
                student_id,
                name,
                course
             FROM students
             WHERE name LIKE ?
             OR student_id LIKE ?
             ORDER BY id DESC"
        );

        $stmt->execute([
            $keyword,
            $keyword
        ]);
    }

    $students =
        $stmt->fetchAll(
            PDO::FETCH_ASSOC
        );

    echo json_encode([
        "success" => true,
        "data" => $students
    ]);

} catch (PDOException $e) {

    error_log(
        $e->getMessage()
    );

    http_response_code(500);

    echo json_encode([
        "success" => false,
        "message" =>
            "Server error"
    ]);
}

?>

17. Dynamic WHERE Conditions

APIs often have optional filters. You can build the conditions dynamically while keeping values parameterized.

$conditions = [];
$params = [];

if ($course !== '') {

    $conditions[] =
        "course = ?";

    $params[] =
        $course;
}

if ($status !== '') {

    $conditions[] =
        "status = ?";

    $params[] =
        $status;
}

The values remain separate from the SQL statement.

18. What Prepared Statements Do Not Automatically Solve

Prepared statements are designed for values. They should not be mistaken for a solution for every type of dynamic SQL.

For example, a client-controlled column name should not simply be placed into:

ORDER BY $sort

Instead, use an allowlist:

$allowedColumns = [
    'id' => 'id',
    'name' => 'name',
    'fee' => 'fee'
];

$sort =
    $_GET['sort'] ?? 'id';

if (!isset(
    $allowedColumns[$sort]
)) {

    $sort = 'id';
}

$column =
    $allowedColumns[$sort];

19. Validate Numeric IDs

Prepared statements protect SQL values, but application-level validation is still important.

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

if ($id === false ||
    $id === null ||
    $id < 1) {

    http_response_code(400);

    echo json_encode([
        "success" => false,
        "message" =>
            "Invalid ID"
    ]);

    exit;
}

20. Use Least Database Privileges

The database account used by an API should have only the permissions it needs.

API
 ↓
Database User
 ↓
Required Permissions Only

Avoid using an unnecessarily powerful database account for a production application.

21. Do Not Expose Database Errors

Avoid sending:

SQLSTATE[42S02]:
Base table or view not found...

Return a safe message:

{
    "success": false,
    "message": "Server error"
}

Technical details should be logged on the server rather than exposed to the API client.

22. SQL Injection and Authentication

Prepared statements help protect SQL queries, but they do not replace authentication and authorization.

HTTPS
  +
Authentication
  +
Authorization
  +
Validation
  +
Prepared Statements
  =
More Secure API

Multiple security controls should work together.

23. SQL Injection and JWT APIs

A JWT can identify the authenticated user, but database queries using user-supplied values still need proper parameterization.

JWT
 ↓
Identify User
 ↓
Validate Input
 ↓
Prepared SQL
 ↓
Database

Authentication does not automatically make SQL queries safe.

24. React Native Request

The React Native application can send search data normally. The server is responsible for safely handling the received value.

const response = await fetch(
    "https://example.com/api/students.php"
    + "?search="
    + encodeURIComponent(search)
);

const result =
    await response.json();

The API should still use a prepared statement for the search value.

25. Axios Request

const response = await axios.get(
    "https://example.com/api/students.php",
    {
        params: {
            search: search
        }
    }
);

console.log(
    response.data
);

Axios handles URL encoding, while the PHP API must still safely parameterize the database query.

26. Test Secure API in Postman

You can test a search API using Postman.

GET
http://localhost/api/students.php?search=rahul

The server should treat the search text as a value and safely use it in the prepared statement.

Also test normal values, empty values, long values, and invalid input.

27. Common SQL Injection Mistakes

  • Concatenating user input directly into SQL.
  • Using raw request values in SQL statements.
  • Returning detailed database errors to clients.
  • Using an unnecessarily powerful database account.
  • Forgetting validation.
  • Assuming authentication alone protects SQL queries.
  • Allowing arbitrary dynamic SQL identifiers.
  • Storing passwords as plain text.

28. Secure SQL Query Checklist

Receive Input
     ↓
Validate Input
     ↓
Prepare SQL
     ↓
Bind Values
     ↓
Execute Query
     ↓
Handle Errors
     ↓
Return Safe JSON

This workflow should become a standard pattern when building PHP REST APIs.

29. Complete SQL Injection Prevention Flow

React Native
      ↓
HTTPS Request
      ↓
PHP REST API
      ↓
Authentication
      ↓
Input Validation
      ↓
Prepared Statement
      ↓
Bound Parameters
      ↓
MySQL
      ↓
Safe JSON Response
      ↓
React Native

SQL injection prevention is one part of a complete API security strategy.

30. SQL Injection Prevention Summary

SQL injection prevention starts with never treating untrusted input as part of an SQL statement. Use PDO prepared statements for values, validate incoming data, use allowlists for dynamic SQL identifiers, protect database credentials, and avoid exposing database errors.

Bad:
SQL + User Input

Good:
SQL + Placeholder
          ↓
       Safe Value

These practices help create safer PHP REST APIs for React Native applications.

📌 Key Points

  • SQL injection can occur when untrusted input is directly added to SQL.
  • Never concatenate user input directly into SQL queries.
  • Use PDO prepared statements for database values.
  • Positional and named placeholders can be used with PDO.
  • Validate IDs, search values, and other incoming data.
  • Prepared statements should be used for SELECT, INSERT, UPDATE, and DELETE operations.
  • Prepared statements do not automatically make dynamic SQL identifiers safe.
  • Use allowlists for dynamic column names and similar SQL identifiers.
  • Do not expose detailed database errors to API clients.
  • Use appropriate database privileges for the API account.
  • Authentication and authorization are still required for protected APIs.
  • Never store passwords as plain text.
  • React Native sends the request, but the server must safely process the input.
  • Postman can be used to test API security and input handling.
  • The next lesson will cover CORS.

🧠 Quick Quiz

Question: What is the recommended way to safely pass user values to a PHP PDO SQL query?