SQL injection happens when external data is joined directly into an SQL statement and changes the intended query structure. In PDO, the main protection is to use prepared statements with placeholders instead of concatenating input into SQL.
<?php
require 'config.php';
$id=filter_input(
INPUT_GET,
'id',
FILTER_VALIDATE_INT
);
if($id===false || $id===null){
exit('Invalid student ID.');
}
$sql="SELECT id,name,class,mark
FROM student
WHERE id=:id";
$stmt=$dbo->prepare($sql);
$stmt->bindValue(
':id',
$id,
PDO::PARAM_INT
);
$stmt->execute();
$row=$stmt->fetch(PDO::FETCH_ASSOC);
The SELECT statement and WHERE condition remain fixed while the value is supplied separately.
Show Table of ContentsSuppose a student ID is received from the query string:
$id=$_GET['id'] ?? '';
$sql="SELECT id,name,class,mark
FROM student
WHERE id=$id";
This is unsafe because the value of $id becomes part of the SQL text itself.
A simple way to understand SQL injection is to look at a text value placed directly into a WHERE condition.
$name=$_GET['name'] ?? '';
$sql="SELECT id,name
FROM student
WHERE name='$name'";
If specially constructed input changes the condition, the final SQL can become logically different from what the programmer intended.
SELECT id,name
FROM student
WHERE name=''
OR '1'='1'
Because the second condition is always true, the query can return records that were never intended to be selected.
The SQL section has a broader introduction to SQL security and safe query design.
With a prepared statement, the SQL structure contains a placeholder and the value is sent separately.
$name=$_GET['name'] ?? '';
$sql="SELECT id,name,class,mark
FROM student
WHERE name=:name";
$stmt=$dbo->prepare($sql);
$stmt->bindValue(
':name',
$name,
PDO::PARAM_STR
);
$stmt->execute();
while($row=$stmt->fetch(PDO::FETCH_ASSOC)){
echo (int)$row['id'].' - ';
echo htmlspecialchars(
(string)$row['name'],
ENT_QUOTES,
'Windows-1252'
).'<br>';
}
The supplied value is treated as data rather than being parsed as part of the SQL syntax.
Both methods can bind parameters. When the value is already available, bindValue() is usually straightforward:
$stmt->bindValue(
':id',
$id,
PDO::PARAM_INT
);
bindParam() binds a variable by reference, which is useful when the variable value may change before execution.
Prepared statements protect the SQL structure, but input should still be validated according to the application's rules.
$id=filter_input(
INPUT_GET,
'id',
FILTER_VALIDATE_INT,
[
'options' => [
'min_range' => 1
]
]
);
if($id===false || $id===null){
exit('Invalid student ID.');
}
Then bind the validated integer:
$stmt->bindValue(
':id',
$id,
PDO::PARAM_INT
);
String input should also use placeholders rather than being quoted and concatenated manually.
$class=trim($_GET['class'] ?? '');
$sql="SELECT id,name,class,mark
FROM student
WHERE class=:class";
$stmt=$dbo->prepare($sql);
$stmt->bindValue(
':class',
$class,
PDO::PARAM_STR
);
$stmt->execute();
Authentication queries are especially sensitive. Do not build a login query by placing the entered userid and password directly into SQL.
A safer pattern is to retrieve the account by userid using a prepared statement:
$userid=trim($_POST['userid'] ?? '');
$password=$_POST['password'] ?? '';
$sql="SELECT id,userid,password,name,status
FROM pdo_admin
WHERE userid=:userid";
$stmt=$dbo->prepare($sql);
$stmt->bindValue(
':userid',
$userid,
PDO::PARAM_STR
);
$stmt->execute();
$user=$stmt->fetch(PDO::FETCH_ASSOC);
if(
$user &&
password_verify(
$password,
$user['password']
)
){
echo 'Login successful.';
}else{
echo 'Invalid login details.';
}
password_hash() and verify them with password_verify(). Do not store or compare plaintext passwords in new applications.PDO placeholders are for data values. They cannot normally be used to bind SQL identifiers such as table names, column names or keywords.
This does not work as intended:
$sql="SELECT :column
FROM student";
If a column or sort direction must be selected dynamically, use an allowlist:
$allowed_columns=[
'name',
'class',
'mark'
];
$column=$_GET['sort'] ?? 'name';
if(
!in_array(
$column,
$allowed_columns,
true
)
){
$column='name';
}
$sql="SELECT id,name,class,mark
FROM student
ORDER BY $column";
$stmt=$dbo->query($sql);
Here the SQL identifier comes only from a predefined list controlled by the application.
Older SQL injection examples often show input containing a second statement such as:
10; DROP TABLE student
Whether multiple statements can actually execute through one PDO call depends on the driver and connection configuration. Therefore, this should not be used as the main explanation of SQL injection.
The core problem exists even when a second statement cannot run: direct concatenation can still change a WHERE condition, reveal unintended rows, bypass application logic or alter other database operations.
For the SQL command itself, see DROP TABLE.
Prepared statements and validation solve different problems:
For example, a prepared statement can safely bind the integer -500, but the application may still reject it if a student ID must be positive.
The existing Plus2net demonstration page can be used to study how unsafe login-style input changes query logic.
Avoid code such as:
$sql="SELECT id,name
FROM student
WHERE id=$id";
Use a placeholder instead.
When using PDO prepared statements, do not switch to MySQLi escaping functions. Supply the value through the PDO parameter interface.
Validation is important, but it should not replace parameterized queries. Use both where appropriate.
Placeholders are for data values, not SQL identifiers. Use an allowlist for dynamic columns, table names or sort directions.
Do not output password hashes or authentication credentials while demonstrating database records.
Database errors can reveal SQL structure, table names and other internal details. Log technical errors and show a generic public message.
Prepared statements protect query structure for bound values. Authentication, authorization, CSRF protection, validation, output escaping and password handling are separate security concerns.
Download the complete PDO examples from the main PDO tutorial.
Sample Student Table SQL DumpSQL injection occurs when supplied data becomes part of the SQL structure and changes the intended database query.
PDO prepared statements use placeholders so supplied values are sent separately from the SQL statement and treated as data rather than SQL syntax.
Yes. Prepared statements protect SQL structure, while validation checks whether supplied values satisfy the application's rules.
No. Placeholders are intended for data values. Dynamic SQL identifiers should be selected from an application-controlled allowlist.
Both can safely bind values to placeholders. bindValue() supplies the current value directly, while bindParam() binds a variable by reference.
No. Retrieve the account using a prepared userid query and verify a stored password hash using password_verify().
No. They protect SQL query structure for bound values, but applications still need validation, authentication, authorization, safe output, CSRF protection where appropriate and secure password handling.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.