PHP 8.6 mysqli::quote_string(): Safer Manual Escaping for MySQL
PHP 8.6 adds mysqli::quote_string(), a single-quote-only escaper modeled on PDO::quote(). See the injection gap it closes and how to migrate safely.
PHP 8.6 adds a small function with a big job: mysqli::quote_string(), plus its procedural twin mysqli_quote_string(). It escapes a string the way real_escape_string() does, then wraps the result in single quotes for you. That sounds minor until you see the injection hole it closes.
If you still maintain code that builds SQL by hand, this is worth ten minutes of your time. Prepared statements remain the right answer for almost everything, and I will say so again below. But plenty of real projects escape manually, and for them this function is a genuine safety upgrade.
What the RFC adds
The feature comes from Kamil Tekiela’s New function mysqli_quote_string RFC. It passed 15 to 2 with 4 abstentions and is marked Implemented. PHP.Watch lists it as a PHP 8.6 feature.
The signatures are:
<?php
// Method form
public function quote_string(string $string): string {}
// Procedural form
function mysqli_quote_string(mysqli $mysql, string $string): string {}
It mirrors PDO::quote(). The RFC’s own example shows the behavior:
<?php
"'Don\\'t!'" === $link->quote_string("Don't!");
Notice the outer single quotes in the result. You do not add them yourself.
The problem with real_escape_string()
The RFC makes a blunt claim: the only valid use of mysqli::real_escape_string() is inside quotation marks that you add by hand, so you end up writing sprintf calls everywhere. Forget the quotes and you have a vulnerability. The long, awkward name does not help.
The bigger issue is double quotes. The RFC states that PHP’s real_escape_string() has a well-known injection vulnerability when you wrap the result in double quotation marks. The RFC demonstrates it with NO_BACKSLASH_ESCAPES enabled:
<?php
$mysqli->query('CREATE TABLE IF NOT EXISTS foo (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255))');
$mysqli->query('INSERT INTO foo (name) VALUES ("normal")');
$mysqli->query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
$value = '" OR 1=1 -- foo';
$sql = sprintf('SELECT id FROM foo WHERE name="%s"', $mysqli->real_escape_string($value));
$result = $mysqli->query($sql);
var_dump($result->fetch_column()); // string(1) "1"
The attacker’s input matched a row it should not have. Swap in the new method and the same input is harmless:
<?php
$sql = sprintf('SELECT id FROM foo WHERE name=%s', $mysqli->quote_string($value));
$result = $mysqli->query($sql);
var_dump($result->fetch_column()); // bool(false)
Notice that the format string no longer contains quotes around %s. That is the migration trap, and I will come back to it.
Why single quotes only
MySQL’s C API fixed this problem on its side with mysql_real_escape_string_quote(), which takes the quote character as an extra argument. The RFC chose a different path. Like PDO, quote_string() offers no way to pick the quote character, so it always uses a single quote. Double-quoted strings are non-standard SQL anyway, and removing the choice removes the bug.
The name is deliberate
Why quote_string and not just quote? The RFC explains that the name tells you the function is for strings only. In non-strict mode PHP will coerce an integer or float into the string parameter, which the RFC calls a programming error. It adds that this newbie mistake was repeated many times in mysqli code and spread by low-quality online tutorials. Putting “string” in the name is a nudge. If you need an integer in a query, cast it with (int) or bind it.
A practical example
Here is a small repository class for a PHPTek.tv session lookup. It keeps manual escaping because it builds a query that gets logged for support staff, which is the sort of case the RFC calls out for tools like phpMyAdmin that must display complete queries.
<?php
declare(strict_types=1);
final class SessionSearch
{
public function __construct(private mysqli $db) {}
public function buildQuery(string $speaker, int $year): string
{
return sprintf(
'SELECT id, title FROM sessions WHERE speaker = %s AND year = %d',
$this->db->quote_string($speaker),
$year,
);
}
}
$db = new mysqli('localhost', 'app', 'secret', 'phptek');
$db->set_charset('utf8mb4');
$search = new SessionSearch($db);
echo $search->buildQuery("O'Reilly", 2026);
// SELECT id, title FROM sessions WHERE speaker = 'O\'Reilly' AND year = 2026
Two habits worth keeping. The %d placeholder forces the year through integer formatting, and the connection charset is set explicitly. Escaping depends on the connection character set, so a mismatch between client and server charsets undermines any escaping function.
Migrating without creating a hole
The RFC admits a real risk here. Moving from real_escape_string() to quote_string() means deleting the quotes you placed by hand. If you delete the quotes but forget to swap the function, you ship an injection. If you swap the function but keep the quotes, you get a syntax error, which is the safe failure.
A migration checklist I would use:
- Search for every call to
real_escape_stringandmysqli_real_escape_string. - Change one call site at a time, removing the surrounding quotes in the same edit.
- Run your tests with a value containing a single quote and a value containing a double quote.
- Review any wrapper in your framework or library that exposes escaping without adding quotes. The RFC warns these will be harder to adopt and are a good chance to review usage.
Do not do a blind regex replace. A mechanical edit is exactly how the quotes get dropped.
Here is a quick PHPUnit-style check you can drop into your suite:
<?php
public function testQuoteStringHandlesHostileInput(): void
{
$hostile = '" OR 1=1 -- ';
$this->db->query('SET SQL_MODE="NO_BACKSLASH_ESCAPES"');
$sql = sprintf('SELECT COUNT(*) FROM sessions WHERE speaker = %s', $this->db->quote_string($hostile));
$this->assertSame('0', (string) $this->db->query($sql)->fetch_column());
}
Should you still use prepared statements?
Yes. The RFC itself says it hopes most users choose prepared statements to avoid injection entirely. quote_string() is for the cases where manual escaping is a design choice or a requirement. If you are writing a new feature with user input going into a query, use mysqli_stmt or PDO with bound parameters, or let your framework’s query builder do it.
What about deprecating real_escape_string()?
The RFC’s Future Scope says real_escape_string() should ideally be deprecated in the next minor version after this lands, but that is explicitly not part of the RFC and its timing is open. Do not expect a deprecation notice in 8.6. Plan to migrate on your own schedule. If you want to see what 8.6 does deprecate, I covered that in PHP 8.6 Deprecations: Build an Upgrade Scanner.
Takeaways
mysqli::quote_string() does one thing: it escapes and quotes in a single call, always with single quotes. That removes the forgotten-quotes mistake and the double-quote injection described in the RFC. It is not a replacement for prepared statements, and it requires careful migration. But for legacy mysqli code, it is the easiest security win in PHP 8.6.
If you want to try it before your production upgrade, run your test suite against the 8.6 release candidates, as described in Test PHP 8.6 RC with Docker and GitHub Actions.
Sources
- PHP RFC: New function mysqli_quote_string, wiki.php.net
- PHP 8.6 RFC: New function mysqli_quote_string, PHP.Watch
- mysqli_quote_string Function, PHP.Watch Codex