Introduction: Why are SQL injections still relevant?
\n\nImagine: your website runs like clockwork, users register, leave comments, place orders. And suddenly — the database ends up in the hands of an attacker. Not because hackers are geniuses, but because someone forgot to put a question mark in an SQL query.
\n\nSQL injections are not an archaic threat from the 2000s. In 2024, OWASP still ranks them in the top 3 most dangerous web application vulnerabilities. And the question "How can I prevent SQL injection in PHP?" on Stack Overflow has over 1.5 million views — and continues to grow. So, the problem is alive.
In this article, we will figure out how to firmly close the door to SQL injections in PHP in 2025. No magic, no complex frameworks, just clean code and best practices.
\n\nWhat is an SQL injection in simple terms?
\n\nAn SQL injection is when user input becomes part of an SQL command, not just data. A real-life example:
\n\n<?php\n// BAD CODE — never do this!\n$username = $_POST['username'];\n$query = "SELECT * FROM users WHERE username = '$username'";\n$result = mysqli_query($conn, $query);\n?>\n\nIf an attacker enters the string ' OR '1'='1 into the username field, the query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1'\n\nThis returns all users. And if they enter '; DROP TABLE users; -- — you could lose the entire table. Scary? You bet.
Best practices for protecting against SQL injections in PHP 2025
\n\n1. Use PDO with prepared statements (this is the standard)
\n\nPDO (PHP Data Objects) is a universal interface for working with databases. Its main advantage: it separates SQL logic and data. You write a query with placeholders, and PDO takes care of safe escaping itself.
\n\n<?php\n// GOOD CODE — use PDO\n$dsn = 'mysql:host=localhost;dbname=testdb;charset=utf8mb4';\n$pdo = new PDO($dsn, $user, $pass, [\n PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n PDO::ATTR_EMULATE_PREPARES => false, // disable emulation\n]);\n\n$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');\n$stmt->execute(['email' => $_POST['email']]);\n$user = $stmt->fetch();\n?>\n\nWhy does this work? The database receives the query structure separately from the data. Even if the user enters malicious SQL, it will be interpreted as a regular string, not as part of the command.
\n\n2. Mysqli with prepared statements (alternative)
\n\nIf you only work with MySQL, you can use mysqli — but only with prepared statements, not with escaping via mysqli_real_escape_string().
<?php\n$mysqli = new mysqli('localhost', 'user', 'pass', 'testdb');\n$stmt = $mysqli->prepare('INSERT INTO comments (user_id, text) VALUES (?, ?)');\n$stmt->bind_param('is', $userId, $commentText);\n$stmt->execute();\n?>\n\nImportant: in 2025, mysqli_real_escape_string() still exists, but relying on it for protection is a bad idea. It only escapes strings, but does not protect against injections in numeric fields or in LIKE operators.
3. Filtering and validation — the second line of defense
\n\nPrepared statements are the foundation. But add data type filtering on top of them. If a number is expected — check that it is a number. If an email — check the format.
\n\n<?php\n$id = filter_input(INPUT_POST, 'user_id', FILTER_VALIDATE_INT);\nif ($id === false) {\n throw new InvalidArgumentException('ID must be an integer');\n}\n\n$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id');\n$stmt->execute(['id' => $id]);\n?>\n\nCombine