How to prevent SQL injection vulnerabilities in PHP applications

SQL injection remains a serious risk for PHP applications, but it’s entirely preventable with the right techniques. This guide covers practical methods to protect your code and data from real-world attacks, including prepared statements, stored procedures, and DAST testing.

How to prevent SQL injection vulnerabilities in PHP applications

Even after decades, SQL injection (SQLi) remains one of the most severe and common threats in web development. This type of security vulnerability allows attackers to manipulate SQL queries through user input, potentially exposing sensitive information, bypassing authentication, or compromising the entire database server.

For PHP developers, understanding how SQLi works and knowing how to prevent it is critical to building secure web applications. This post explains what SQL injection is, shows insecure patterns, and walks through secure techniques using PHP’s PDO and MySQLi extensions.

What is SQL injection and why is it dangerous?

SQL injection happens when user-supplied data is embedded directly into SQL statements without proper safeguards. Malicious input can alter the intended SQL code, leading to unauthorized access, data theft, or manipulation of database content. For example, consider this vulnerable login implementation in PHP, where whatever values the user supplies for the username and password are directly concatenated with the SQL query:

$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

The application then performs rudimentary authentication by checking if a database query for the supplied username and password pair returns results or an empty result set.

Note that in real production applications, you should never send, store, or compare plaintext passwords—only password hashes should be compared.

Assuming the attacker knows a default user called admin exists, all they need to do to log in is enter admin' -- for the username and any value for the password (basically anything the application logic will accept). The resulting SQL query becomes:

SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'

The -- starts a comment, so the rest of the query is ignored. The database duly executes:

SELECT * FROM users WHERE username = 'admin'

As long as the admin user exists, this will always return a non-empty result set, in effect bypassing the password check entirely and granting unauthorized access to the admin account.

SQLi can also be used by malicious hackers to enumerate users, dump sensitive information, or execute destructive actions depending on the specific vulnerability and the permissions of the database user. Attackers can use a variety of SQL commands such as UNION SELECT to extract data regardless of the injection point in the original query or DROP TABLE to delete data.

Why escaping alone is not enough

Some older code attempts to use mysqli_real_escape_string() to sanitize input as an SQLi prevention measure:

$username = mysqli_real_escape_string($conn, $_GET['user']);
$query = "SELECT * FROM users WHERE name = '$username'";

While this reduces the risk of injection, it’s not foolproof. Escaping alone cannot protect against every variation of SQL injection, especially with multibyte encodings or misconfigured database connections. It also does nothing for structural parts of a query, like column names or table names.

SQL injection prevention 101: Use prepared statements and parameterized queries

The best defense against SQL injection in PHP is to use parameterized queries with prepared statements. These separate user data from SQL code, ensuring that any special characters and SQL keywords in user input cannot alter the query structure.

Note that while both examples below use hardcoded credentials for simplicity, in a real application, you should load database connection details from environment variables (using getenv()) or a secure configuration source to avoid exposing sensitive information.

Using PDO (PHP Data Objects)

PDO offers a consistent interface for accessing databases and is preferred for new PHP projects. It supports named placeholders and built-in error handling. Here’s an example that queries for users matching a specified numeric status:

// Hardcoded credentials for illustration only
$pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "pass");
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = :name AND status = :status");
$stmt->bindParam(':name', $_GET['user'], PDO::PARAM_STR);
$stmt->bindParam(':status', $status, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll();

Named placeholders make the code more secure and readable, while binding parameters by data type using bindParam() limits the potential for abuse (and also plain data entry errors).

Using MySQLi

MySQLi is another secure method available for running database queries in PHP. It supports positional placeholders and is ideal when PDO is not used. The following example shows a MySQLi version of the PDO code from above:

// Hardcoded credentials for illustration only
$mysqli = new mysqli("localhost", "user", "pass", "testdb"); 

if ($mysqli->connect_error) {
    die("Connection failed: " . $mysqli->connect_error);
}

$stmt = $mysqli->prepare("SELECT * FROM users WHERE name = ? AND status = ?");
$user = $_GET['user'];
$status = 1;
$stmt->bind_param("si", $user, $status); // "s" for string, "i" for integer
$stmt->execute();
$result = $stmt->get_result();

Here, the bind_param() function uses placeholders and ensures correct type handling, protecting both numeric and string inputs.

Additional techniques to prevent SQLi in PHP applications

Parameterization is non-negotiable when it comes to preventing SQL injection, but also following a combination of more general good practices will make your application more secure and reliable overall. Keep in mind that none of these in isolation will be enough to protect against SQLi.

1. Validate and sanitize user input

Always apply input validation to check that user data matches expected types, formats, and values. A few general rules to follow:

  • Validate expected value types, so if a parameter can only be an integer, you could use filter_var($input, FILTER_VALIDATE_INT).
  • For choices from a closed and known set, use an allowlist (whitelist) to only accept those values.
  • Avoid relying purely on filtering out certain keywords (blacklisting) or removing SQL special characters.

2. Avoid dynamic SQL

Dynamic SQL (building query strings from user input) should be avoided as a general rule. If absolutely necessary (like when you have dynamic column names), validate input strictly or use mapping to predefined values, for example:

$columns = ['name', 'email', 'created_at'];
$sortColumn = in_array($_GET['sort'], $columns) ? $_GET['sort'] : 'name';
$query = "SELECT * FROM users ORDER BY $sortColumn";

This example shows how to safely handle dynamic column names in SQL queries. Since parameterized queries can’t be used for table or column identifiers, the only secure way to build such queries is to validate the user input against a predefined allowlist of expected column names. This prevents attackers from injecting malicious SQL through the sort parameter.

3. Use stored procedures with parameters

Stored procedures are precompiled SQL routines stored on the database server. They can encapsulate complex logic and, when used with parameterized inputs, offer a secure way to interact with your database. 

Here’s an example showing how to define a simple stored procedure in MySQL that retrieves user records by username. This code would typically be executed directly in the MySQL command-line client or a database management tool like phpMyAdmin or MySQL Workbench:

CREATE PROCEDURE GetUser(IN uname VARCHAR(50))
BEGIN
    SELECT * FROM users WHERE name = uname;
END

Call it from PHP using:

$stmt = $pdo->prepare("CALL GetUser(:uname)");
$stmt->bindParam(':uname', $_GET['user']);
$stmt->execute();

While stored procedures add an extra level of security, it’s still possible to use them insecurely. Be careful not to construct dynamic SQL inside stored procedures without safe parameter handling—otherwise you’re merely moving the injection point from the application to the stored procedure.

4. Implement careful error handling

Never expose raw database errors in production. Turn off detailed error messages and log them securely. This prevents attackers from gaining insights into query structure or database behavior:

ini_set('display_errors', 0);
error_reporting(0);

// Log errors to a secure file outside the web root
ini_set('log_errors', 1);
ini_set('error_log', '/var/log/php_errors.log');
error_log("Application error at " . $_SERVER['REQUEST_URI']);

5. Apply the principle of least privilege

The database user used by your PHP application should have only the minimal permissions needed for its role. While it’s often convenient during development to access your database with full admin rights and do whatever you need to do without worrying about user permissions, running a production app like that can be extremely dangerous. To limit the damage in case an attacker does manage to exploit an injection, you shouldn’t allow any operations not needed by the application, especially things like DROP, GRANT, or access to administrative tables.

Test your application for SQL injection vulnerabilities

Even if your code follows all best practices, it’s still essential to verify that your application is secure in practice, not just in theory. Testing for SQL injection vulnerabilities should be a routine and automated part of your development and security processes.

Why DAST matters

Dynamic application security testing (DAST) tools analyze your application from the outside just like a real attacker would. They simulate malicious inputs in runtime environments and attempt to exploit vulnerabilities such as SQL injection without requiring access to your source code.

A well-designed DAST solution can:

  • Automatically identify exploitable SQL injection points, including blind SQLi and time-based conditions
  • Test complex workflows, forms, API endpoints, and even JavaScript-driven input fields in modern web applications
  • Provide accurate results by safely exploiting and confirming real vulnerabilities instead of flooding developers with false positives
  • Offer actionable remediation guidance and detailed proof-of-exploit data to help teams fix issues faster

Unlike static analysis tools that often flag theoretical issues, DAST focuses on what’s actually exploitable in your running application. This makes it especially valuable for catching issues that only become evident in a fully functioning environment, from injections to misconfigurations.

Combine DAST with manual testing

While automated tools like DAST cover broad areas efficiently, manual testing still plays a role, especially for edge cases or logic-heavy components. Tools like sqlmap can be useful for penetration testers and advanced users looking to simulate real-world attack scenarios or chain multiple vulnerabilities.

Continuous testing is key

Security isn’t a one-time task. Integrate DAST into your CI/CD pipeline to catch regressions and ensure that newly introduced code doesn’t open up fresh attack vectors. Frequent scanning combined with timely remediation and secure coding practices is the most effective defense against SQL injection and many other vulnerabilities.

At a glance: Secure your PHP applications against SQLi

SQL databases are not going away and neither is SQL injection, so securing PHP apps against SQLi attacks is not optional—it’s fundamental. Following these rules should help you ensure protection:

  • Always use parameterized queries (for PHP, that usually means PDO or MySQLi)
  • Always regularly test your applications with automated and manual tools
  • Validate and sanitize user input rigorously
  • Avoid dynamic SQL or validate its components
  • Implement strong error handling
  • When possible, use stored procedures with parameter binding
  • Restrict database user permissions

By embedding these practices into your PHP development and operations workflow, you can significantly reduce the risk of SQL injection vulnerabilities and build more resilient web applications overall.

For more advanced examples of SQL injection attacks, refer to the Invicti SQL Injection Cheat Sheet.

Zbigniew Banach

About the Author

Zbigniew Banach - Technical Content Lead & Managing Editor

Cybersecurity writer and blog managing editor at Invicti Security. Drawing on years of experience with security, software development, content creation, journalism, and technical translation, he does his best to bring web application security and cybersecurity in general to a wider audience.