From 1b84c85148495587ffb32ac5096ba18fcac32802 Mon Sep 17 00:00:00 2001 From: Jahnvi Thakkar Date: Tue, 21 Jul 2026 11:44:48 +0530 Subject: [PATCH] Fix binary parameter truncation at embedded NUL in PDO emulated prepares (CWE-626) pdo_sqlsrv_dbh_quote hex-encoded binary parameters but stopped at the first NUL byte, silently truncating data bound with SQLSRV_ENCODING_BINARY under emulated prepares. Encode the full length in both PHP version paths and add a regression test. (MSRC 125656) --- CHANGELOG.md | 5 ++ source/pdo_sqlsrv/pdo_dbh.cpp | 8 ++- ...tement_bindParam_binary_embedded_null.phpt | 71 +++++++++++++++++++ 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 test/functional/pdo_sqlsrv/pdostatement_bindParam_binary_embedded_null.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b8fca7bd..ecc52923e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [Unreleased] + +### Security +- Fixed silent truncation of binary parameters containing embedded NUL (0x00) bytes when using PDO emulated prepares with `PDO::SQLSRV_ENCODING_BINARY` (CWE-626). The hex-encoding loops in `pdo_sqlsrv_dbh_quote` treated binary data as a C string and stopped at the first NUL, causing only the pre-NUL prefix to be sent to the server. All bytes are now encoded. + ## 5.13.0 - 2026-02-27 Updated PECL release packages. Here is the list of updates: diff --git a/source/pdo_sqlsrv/pdo_dbh.cpp b/source/pdo_sqlsrv/pdo_dbh.cpp index d9f7cceb8..63755aafe 100644 --- a/source/pdo_sqlsrv/pdo_dbh.cpp +++ b/source/pdo_sqlsrv/pdo_dbh.cpp @@ -1820,7 +1820,9 @@ zend_string* pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_ const zend_string (*quoted)[pos++] = '0'; (*quoted)[pos++] = 'x'; - for (size_t index = 0; index < unquoted_len && unquoted[index] != '\0'; ++index) { + // Binary data may contain embedded NUL (0x00) bytes, so the loop must not + // stop at the first NUL; encode all unquoted_len bytes (CWE-626 truncation fix) + for (size_t index = 0; index < unquoted_len; ++index) { // On success, snprintf returns the total number of characters written // On failure, a negative number is returned // The generated string has a length of at most len - 1, so @@ -1849,7 +1851,9 @@ zend_string* pdo_sqlsrv_dbh_quote(_Inout_ pdo_dbh_t* dbh, _In_ const zend_string quoted[pos++] = 'x'; char *p = quoted; - for (size_t index = 0; index < unquoted_len && unquoted_str[index] != '\0'; ++index) { + // Binary data may contain embedded NUL (0x00) bytes, so the loop must not + // stop at the first NUL; encode all unquoted_len bytes (CWE-626 truncation fix) + for (size_t index = 0; index < unquoted_len; ++index) { // On success, snprintf returns the total number of characters written // On failure, a negative number is returned // The generated string has a length of at most len - 1, so diff --git a/test/functional/pdo_sqlsrv/pdostatement_bindParam_binary_embedded_null.phpt b/test/functional/pdo_sqlsrv/pdostatement_bindParam_binary_embedded_null.phpt new file mode 100644 index 000000000..c96d9e3e9 --- /dev/null +++ b/test/functional/pdo_sqlsrv/pdostatement_bindParam_binary_embedded_null.phpt @@ -0,0 +1,71 @@ +--TEST-- +PDOStatement::bindParam binary data with embedded NUL bytes is not truncated (emulated prepares) +--DESCRIPTION-- +Regression test for the NUL-byte truncation issue (CWE-626) in pdo_sqlsrv_dbh_quote. +With PDO::ATTR_EMULATE_PREPARES enabled and PDO::SQLSRV_ENCODING_BINARY, PDO::quote builds +the hex SQL literal client-side. Binary parameters that contain embedded NUL (0x00) bytes +must be fully hex-encoded rather than being silently truncated at the first NUL byte. +Mirrors pdostatement_bindParam_empty_binary.phpt. +--SKIPIF-- + +--FILE-- +prepare("SELECT DATALENGTH(?)", array(PDO::ATTR_EMULATE_PREPARES => true)); + $stmt->bindParam(1, $binary, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $stmt->execute(); + $len = intval($stmt->fetchColumn()); + if ($len !== $expectedLen) { + echo "Unexpected DATALENGTH: got $len, expected $expectedLen\n"; + } + + // 2) Round-trip the value through a varbinary column and compare byte-for-byte. + $tableName = "pdoBinaryEmbeddedNull"; + $colMetaArr = array(new ColumnMeta("varbinary(16)", "VarBinaryCol")); + createTable($conn, $tableName, $colMetaArr); + + $insert = $conn->prepare("INSERT INTO $tableName (VarBinaryCol) VALUES (?)", array(PDO::ATTR_EMULATE_PREPARES => true)); + $insert->bindParam(1, $binary, PDO::PARAM_LOB, 0, PDO::SQLSRV_ENCODING_BINARY); + $insert->execute(); + + $select = $conn->query("SELECT VarBinaryCol, DATALENGTH(VarBinaryCol) AS Len FROM $tableName"); + $row = $select->fetch(PDO::FETCH_NUM); + if (intval($row[1]) !== $expectedLen) { + echo "Unexpected stored DATALENGTH: got $row[1], expected $expectedLen\n"; + } + if ($row[0] !== $binary) { + echo "Round-tripped binary does not match original\n"; + var_dump(bin2hex($row[0])); + } + + dropTable($conn, $tableName); + unset($select); + unset($insert); + unset($stmt); + unset($conn); + + echo "Done\n"; +} catch (PDOException $e) { + var_dump($e); + exit; +} +?> +--EXPECT-- +Done