SQL injection in PHP is an attack where text a visitor types ends up inside a database query and runs as SQL. The attacker can then read, change or delete your data. You prevent it with prepared statements, a database user with limited rights and a current PHP release.

That last part got urgent on July 30, 2026, when PHP fixed an injection flaw inside its own PostgreSQL functions. Careful code that used those functions on an older PHP build was still open.

On this page

What is SQL injection?

SQL injection is a way to attack a website through its database. The site keeps customers, orders and logins in a database and asks for them in a language called SQL. If the code builds those requests by pasting in what a visitor typed, the visitor can type SQL instead of a name. The database runs it with every permission the website holds.

A kitchen that lets guests write on the order slip has this problem. One guest adds a line under the burger: also bring me the reservation book. The kitchen can't tell the waiter's words from the guest's, so it hands the book over. A prepared statement is a printed form with fixed boxes, and the kitchen reads the dish box as a dish and nothing else.

The damage depends on what the site's database account may do. OWASP's page on SQL injection attacks lists reading sensitive data, changing or deleting records and shutting the database down. In some cases the attacker can send commands to the operating system. It adds that the attack is common in PHP and ASP apps, because older ways of calling the database are still in wide use there.

For an online store, that means copied customer addresses and prices set to one cent overnight. It can also mean a month of deleted orders, or a hidden admin account left for later. None of it needs a password.

What SQL injection in PHP looks like in code

PHP's manual page on SQL injection says the bug appears when code glues raw input into an SQL statement. This customer lookup does exactly that.

// Unsafe: the email is pasted into the SQL text
$email = $_POST['email'];
$sql = "SELECT id, name FROM customers WHERE email = '$email'";
$result = $mysqli->query($sql);

A normal visitor types an address and gets one row back. An attacker types ' OR '1'='1 instead. The query becomes WHERE email = '' OR '1'='1', which is true for every row, so the database returns every customer.

Other inputs go further. An attacker can close the string and add UNION SELECT to pull admin users onto the page. Blind injection works even when the page shows no data. The attacker asks yes-or-no questions and watches whether the page or its response time changes.

How to prevent SQL injection in PHP

Send visitor input to the database as data, apart from the SQL command. Prepared statements do this. The same PHP manual page calls binding all data through them the recommended way to avoid it. For any other part of a query that changes with input, it says to check against a list of allowed values.

All three extensions below support placeholders, so keep the one your code already uses.

ExtensionDatabasesPlaceholdersSafe call for inputAvoid with input
PDOMySQL, MariaDB, PostgreSQL, SQLite and others:name or ?prepare() then execute()query() with input pasted in
mysqliMySQL and MariaDB?prepare(), bind_param(), execute(), or execute_query() on PHP 8.2+String-built queries; real_escape_string() as the main defense
pgsqlPostgreSQL$1, $2pg_query_params(), or pg_prepare() with pg_execute()pg_query() on a built string; pg_insert() and its siblings on unpatched PHP

Allow-lists, a database user with few rights and a current PHP back them up. The OWASP SQL Injection Prevention Cheat Sheet lists prepared statements as defense option 1. It marks escaping all input as strongly discouraged, because escaping depends on the SQL dialect and configuration.

How to write PDO prepared statements

PDO prepared statements keep the SQL and the values apart. You write placeholders such as :email or question marks in the query and pass the values separately, but one statement can't mix the two styles.

$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $dbUser, $dbPass);

$stmt = $pdo->prepare('SELECT id, name FROM customers WHERE email = :email');
$stmt->execute([':email' => $_POST['email']]);
$customer = $stmt->fetch(PDO::FETCH_ASSOC);

Whatever the visitor typed, ' OR '1'='1 included, now arrives as a value to compare with the email column. It never becomes part of the command. The PDO manual on prepared statements says an app that uses only prepared statements gets no SQL injection. The one condition is that no other part of the query comes from raw input.

On MySQL, the PDO_MYSQL manual page says the driver emulates prepares by default, so PDO quotes each value into the query on the client. Set PDO::ATTR_EMULATE_PREPARES to false if you want the server to receive values separately.

Laravel builds on this. Its docs say the query builder uses PDO parameter binding to protect against SQL injection. Raw methods such as whereRaw() are the exception, since Laravel can't guarantee their safety. Pass values to those in the bindings array.

PHP prepared statements with mysqli

The mysqli extension works only with MySQL and MariaDB, and many older PHP apps and plugins use it. You prepare the SQL with question marks, bind the values with their types, then execute.

$stmt = $mysqli->prepare('UPDATE orders SET status = ? WHERE id = ?');
$stmt->bind_param('si', $status, $orderId);
$stmt->execute();

The string 'si' tells mysqli that the first value is a string and the second an integer. PHP's mysqli guide says bound values travel to the server apart from the query, so they can't interfere with it.

Newer PHP versions cut the steps. Since PHP 8.1 you can pass the values to execute() as an array. PHP 8.2 added execute_query(), which prepares, binds and runs in one call. Both treat each value as a string.

$result = $mysqli->execute_query(
    'SELECT id, total FROM orders WHERE customer_id = ?',
    [$customerId]
);

Replace mysqli_real_escape_string() calls as you touch each query. Every one is a spot where a single missed variable lets the attack back in.

What placeholders can't protect

A placeholder holds one complete value and nothing else. It can't stand in for a table name, a column name or a keyword such as ASC or DESC. So a sortable product list is a common place for injection to survive a cleanup.

Map those choices to a fixed list in your code, as both PHP's docs and OWASP advise:

$columns = ['price', 'name', 'created_at'];
$sort = in_array($_GET['sort'] ?? '', $columns, true) ? $_GET['sort'] : 'created_at';
$dir  = ($_GET['dir'] ?? '') === 'desc' ? 'DESC' : 'ASC';

$stmt = $pdo->prepare("SELECT id, name, price FROM products ORDER BY $sort $dir LIMIT 50");
$stmt->execute();

The visitor picks from your list, and anything else falls back to a safe default. Search boxes need care in a different spot. Put the % wildcards inside the bound value, as in $stmt->execute(["%{$term}%"]), and keep the SQL as LIKE ?.

IN() lists trip people up too, because one placeholder can't hold several values. Build one question mark per item with array_fill(), then pass the array to execute().

What CVE-2026-17543 changed for PostgreSQL sites

CVE-2026-17543 is SQL injection in PHP itself, inside its PostgreSQL extension, rather than in a site's own code. PHP fixed it on July 30, 2026. It affects pg_insert(), pg_update(), pg_select() and pg_delete() on releases before 8.5.9, 8.4.24, 8.3.33 and 8.2.33. PHP's security advisory GHSA-7qpv-r5mr-78m4 rates it High, and NVD scores it 9.8 out of 10.

Those four functions escaped each value and wrapped it in an E'...' string. PostgreSQL's docs explain that a backslash inside that kind of string starts an escape sequence. Since version 9.1, standard_conforming_strings has been on by default, and with it on, the escaping step left backslashes alone. A backslash and a quote could then end the string early, and the rest of the input ran as SQL.

The advisory's own demo sends a crafted name to pg_select() and gets back every row in the table. PHP's patch wraps values in ordinary strings, where a backslash is only a backslash.

The lesson for owners is uncomfortable. A developer could use these functions exactly as documented and still ship the bug. Code on pg_query_params() never went through that escaping step, which is the best argument for placeholders over escape-and-quote helpers.

Which PHP sites the PostgreSQL flaw affects

Only PHP code on PostgreSQL that sends outside input through one of the four functions is exposed. Red Hat's page on the CVE says apps built on PDO, prepared statements or parameterized queries don't have the problem. MySQL and MariaDB sites never reach this code, which covers standard WordPress and WooCommerce installs because WordPress lists only those two databases in its requirements.

Check custom PHP apps on PostgreSQL first, such as booking systems, member portals, internal dashboards and in-house tools. A ten-year-old order form that saves its fields with pg_insert() is the textbook case.

PHP 8.1 reached end of life on December 31, 2025, and its last release was 8.1.34. No official 8.1 build carries this fix. Those sites need a move to a supported branch, such as a PHP 8.1 to 8.4 upgrade. PHP 8.2 gets security fixes only until December 31, 2026.

Wiz's vulnerability database showed no confirmed exploitation in the wild and no CISA KEV listing when checked on October 25, 2026. The October 2026 releases are more pressing. They fix the PHP2Shell remote code execution flaws and include the July fix too.

How to check whether your site is exposed

A developer or your host can run each of these checks without special tools.

  • Find the PHP version the live site runs. The hosting panel shows it, while php -v shows the command-line build, which can differ. At or above 8.5.9, 8.4.24, 8.3.33 or 8.2.33 in its branch, you have the fix. Linux distribution packages can carry it under an older number, so read the package changelog.
  • Search the whole project, vendor and plugin folders included, with grep -rnE 'pg_(insert|update|select|delete)\(' . to find the four functions. No matches means this CVE can't reach you.
  • Run a second search for queries built from request data, grep -rnE '(query|exec)\(.*\$_(GET|POST|REQUEST)' ., to catch the obvious cases. Then read every query that uses a variable.
  • Read the database and web server logs for queries your app never makes. Requests to information_schema or pg_catalog are one sign. Bursts of syntax errors from one form are another.
  • Look for admin accounts nobody created, changed admin emails and new database roles. Check prices, payout details and redirects while you're there.

Probing breaks queries before it lands one, so a run of SQL errors in the logs is an early warning.

PHP security habits that limit the damage

A shop whose database user can touch only its own tables loses those tables in a breach. Give that user superuser rights and the same hole reaches every database on the server. On PostgreSQL, a superuser can even run server commands through COPY with the PROGRAM option.

  • Connect with a database user that has only the rights the site needs. PHP's security docs say never to connect as a superuser or the database owner, and OWASP warns against DBA or admin access for app accounts.
  • Validate input by type. An order ID should pass filter_var($id, FILTER_VALIDATE_INT), and a country should come from your own list. Validation catches junk early but doesn't replace binding.
  • Log database errors and keep them off the page. Set display_errors to Off in production, since error text can reveal table and column names.
  • A web application firewall (WAF) blocks common attack strings and buys time. It doesn't repair the code.
  • Patch PHP, the CMS, themes and plugins on a schedule rather than after a scare.

How to fix an exposed site, step by step

Six steps close the hole, in this order. Changing passwords first wastes the effort while the attacker can still walk back in.

  1. Back up the database and site files, then restore the backup on a test copy to prove it works.
  2. Update PHP to the newest release in your branch, or install your vendor's patched package. The October 2026 builds, 8.5.12, 8.4.27, 8.3.36 and 8.2.35, carry the July fix too. This guide to updating your PHP version covers cPanel, Plesk and command-line servers.
  3. Restart PHP-FPM pools, Apache with mod_php and any queue workers. A long-running process keeps the old code in memory until it restarts. Then confirm the version the live site reports.
  4. Rewrite each query that pastes input into SQL as a prepared statement. Do the same for any call that passes request data to pg_insert() and its siblings.
  5. Read the logs as far back as they go, using the signs from the checks above.
  6. If anything looks wrong, change the database password, admin passwords and any API secrets stored in the database. Delete admin accounts nobody recognizes.

Where to start this week

Start with the pages where SQL injection in PHP code costs a business the most: checkout, login, password reset and any admin search page. Rewrite those queries first, then test each form on a staging copy with a single quote and with ' OR '1'='1. An SQL error or extra rows means the fix isn't done.

An old codebase with hundreds of hand-built queries may be cheaper to fix in one pass. Migrating a custom PHP app to Laravel moves those queries onto the query builder and its PDO bindings.

Send your site address, PHP version and database type to the PHP security and upgrade team for an answer within one business day.

Frequently asked questions

Use prepared statements for every query that includes outside input. In PDO that means prepare() and execute(), in mysqli bind_param() or execute_query(), and on PostgreSQL pg_query_params(). Then add allow-lists for column names and sort order, a low-privilege database user and regular PHP updates.

Use PDO for new code unless the project already standardizes on mysqli. PDO runs the same prepare and execute calls on MySQL, MariaDB, PostgreSQL and SQLite, and it supports named placeholders. The mysqli extension covers only MySQL and MariaDB, but its prepared statements are equally safe when every query uses them.

No, treat it as a fallback for old code. OWASP strongly discourages escaping as a main defense because it depends on the database dialect and configuration. CVE-2026-17543 showed escaping failing inside PHP itself. Bound values travel apart from the SQL, so nothing needs escaping in the first place.

Not standard ones. The flaw lives in PHP's PostgreSQL functions, and WordPress requires MySQL or MariaDB. Update PHP anyway, because the newest release in each supported branch also fixes the October 2026 PHP2Shell flaws. Plugin code still deserves its own query review.

Check the database logs for queries your app never runs and for bursts of syntax errors from one form. Requests for information_schema or pg_catalog are a common sign. Also look for admin accounts nobody created and prices or emails that changed without reason. If you find any, back up, fix the query, then rotate passwords.