From 7ecea948ac719f808484895e2c00052610099456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Wed, 18 Sep 2024 20:14:33 +0200 Subject: [PATCH 1/2] Reproduce unexpected MySQL warnings for binary values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prepared statement emulation layer is handling binary content in a way that creates warnings in MySQL. When analysing the query logs, we saw that the content sent to the server is missing `0x5C` characters when the using emulated prepares. This introduces a minimal test case that reproduces the issue to aid the solution. More info: https://github.com/doctrine/dbal/pull/6522#issuecomment-2340939347 Signed-off-by: Luís Cobucci --- .../pdo_mysql_prepare_emulated_binary.phpt | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt diff --git a/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt b/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt new file mode 100644 index 0000000000000..fe198376d6107 --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt @@ -0,0 +1,44 @@ +--TEST-- +MySQL PDO->prepare(), no warnings should be raised for binary values using emulated PS +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + + $content = '0191D886E6DC73E7AF1FEE7F99EC6235'; + + $statement = $db->prepare('SELECT HEX(?) as test'); + $statement->bindValue(1, hex2bin($content), PDO::PARAM_LOB); + $statement->execute(); + + var_dump($statement->fetchAll(PDO::FETCH_ASSOC)[0]['test'] === $content); + var_dump($db->query('SHOW WARNINGS')->fetchAll(PDO::FETCH_ASSOC)); + + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + $statement2 = $db->prepare('SELECT HEX(?) as test'); + $statement2->bindValue(1, hex2bin($content), PDO::PARAM_LOB); + $statement2->execute(); + + var_dump($statement2->fetchAll(PDO::FETCH_ASSOC)[0]['test'] === $content); + + $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); // SHOW WARNINGS can only be used when PDO::ATTR_EMULATE_PREPARES=true + var_dump($db->query('SHOW WARNINGS')->fetchAll(PDO::FETCH_ASSOC)); + print "done!"; +?> +--EXPECTF-- +bool(true) +array(0) { +} +bool(true) +array(0) { +} +done! From 9fa7ad7cb3647b0b8fe15965fbc7d60df5084e99 Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Tue, 24 Sep 2024 18:09:26 +0200 Subject: [PATCH 2/2] Properly quote binary strings --- ext/pdo_mysql/mysql_driver.c | 29 +++++++++++++------ .../pdo_mysql_prepare_emulated_binary.phpt | 4 +++ .../tests/pdo_mysql_quote_binary.phpt | 28 ++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 ext/pdo_mysql/tests/pdo_mysql_quote_binary.phpt diff --git a/ext/pdo_mysql/mysql_driver.c b/ext/pdo_mysql/mysql_driver.c index f75959fce0c8d..3bdb335a4a99b 100644 --- a/ext/pdo_mysql/mysql_driver.c +++ b/ext/pdo_mysql/mysql_driver.c @@ -309,23 +309,29 @@ static zend_string* mysql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo { pdo_mysql_db_handle *H = (pdo_mysql_db_handle *)dbh->driver_data; bool use_national_character_set = 0; + bool use_binary = 0; size_t quotedlen; - if (H->assume_national_character_set_strings) { - use_national_character_set = 1; - } - if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { - use_national_character_set = 1; - } - if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { - use_national_character_set = 0; + if ((paramtype & PDO_PARAM_LOB) == PDO_PARAM_LOB) { + use_binary = 1; + } else { + if (H->assume_national_character_set_strings) { + use_national_character_set = 1; + } + if ((paramtype & PDO_PARAM_STR_NATL) == PDO_PARAM_STR_NATL) { + use_national_character_set = 1; + } + if ((paramtype & PDO_PARAM_STR_CHAR) == PDO_PARAM_STR_CHAR) { + use_national_character_set = 0; + } } PDO_DBG_ENTER("mysql_handle_quoter"); PDO_DBG_INF_FMT("dbh=%p", dbh); PDO_DBG_INF_FMT("unquoted=%.*s", (int)ZSTR_LEN(unquoted), ZSTR_VAL(unquoted)); - zend_string *quoted_str = zend_string_safe_alloc(2, ZSTR_LEN(unquoted), 3 + (use_national_character_set ? 1 : 0), false); + zend_string *quoted_str = zend_string_safe_alloc(2, ZSTR_LEN(unquoted), + 3 + (use_national_character_set ? 1 : 0) + (use_binary ? 7 : 0), false); char *quoted = ZSTR_VAL(quoted_str); if (use_national_character_set) { @@ -334,6 +340,11 @@ static zend_string* mysql_handle_quoter(pdo_dbh_t *dbh, const zend_string *unquo quoted[1] = '\''; ++quotedlen; /* N prefix */ + } else if (use_binary) { + quotedlen = mysql_real_escape_string_quote(H->server, quoted + 8, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), '\''); + memcpy(quoted, "_binary'", 8); + + quotedlen += 7; /* _binary prefix */ } else { quotedlen = mysql_real_escape_string_quote(H->server, quoted + 1, ZSTR_VAL(unquoted), ZSTR_LEN(unquoted), '\''); quoted[0] = '\''; diff --git a/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt b/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt index fe198376d6107..9d29129984388 100644 --- a/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt +++ b/ext/pdo_mysql/tests/pdo_mysql_prepare_emulated_binary.phpt @@ -13,6 +13,10 @@ MySQLPDOTest::skip(); $db = MySQLPDOTest::factory(); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + // Force the connection to utf8, which is enough to make the test fail + // MySQL 5.6+ would be required for utf8mb4 + $db->exec("SET NAMES 'utf8'"); + $content = '0191D886E6DC73E7AF1FEE7F99EC6235'; $statement = $db->prepare('SELECT HEX(?) as test'); diff --git a/ext/pdo_mysql/tests/pdo_mysql_quote_binary.phpt b/ext/pdo_mysql/tests/pdo_mysql_quote_binary.phpt new file mode 100644 index 0000000000000..9c81f0849096c --- /dev/null +++ b/ext/pdo_mysql/tests/pdo_mysql_quote_binary.phpt @@ -0,0 +1,28 @@ +--TEST-- +MySQL PDO->quote(), properly handle binary data +--EXTENSIONS-- +pdo_mysql +--SKIPIF-- + +--FILE-- +setAttribute(PDO::ATTR_EMULATE_PREPARES, true); + + // Force the connection to utf8, which is enough to make the test fail + // MySQL 5.6+ would be required for utf8mb4 + $db->exec("SET NAMES 'utf8'"); + + $content = "\xC3\xA1\xC3"; + $quoted = $db->quote($content, PDO::PARAM_LOB); + + var_dump($quoted); + var_dump($db->query("SELECT HEX({$quoted})")->fetch(PDO::FETCH_NUM)[0]); +?> +--EXPECTF-- +string(%d) "_binary'%s'" +string(6) "C3A1C3"