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.
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.
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:
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.
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.
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.
PDO provides prepared statements through prepare() and
execute().
$stmt = $pdo->prepare(
"SELECT *
FROM users
WHERE email = ?"
);
$stmt->execute([
$email
]);
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.
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.
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.
$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.
Prepared statements should also be used when updating records.
$stmt = $pdo->prepare(
"UPDATE students
SET name = ?
WHERE id = ?"
);
$stmt->execute([
$name,
$id
]);
$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.
$stmt = $pdo->prepare(
"INSERT INTO students
(name, email, course)
VALUES (?, ?, ?)"
);
$stmt->execute([
$name,
$email,
$course
]);
This separates the SQL statement from the supplied values.
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.
$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.
<?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"
]);
}
?>
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.
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];
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;
}
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Question: What is the recommended way to safely pass user values to a PHP PDO SQL query?