【问题标题】:PDO::quote() for SQL Server mis-quotes strings containing ASCII NULPDO::quote() 用于 SQL Server 错误引用包含 ASCII NUL 的字符串
【发布时间】:2017-09-07 15:47:06
【问题描述】:

我正在尝试使用 pdo_sqlsrv 将 ASCII NUL 字符(\0 aka U+0000)从 PHP 插入 SQL Server 数据库。这是处理 PHP 序列化字符串的要求,其中包含 NUL 字符来表示私有/受保护的变量。

但是,PDO::quote() 有一些东西会破坏字符串。

要重现的代码(将DBNAME、USERNAME 和PASSWORD 替换为适当的值):

<?php

try {
    $dsn = 'sqlsrv:Server=.\SQLEXPRESS;Database=DBNAME';
    $user = 'USERNAME';
    $pass = 'PASSWORD';

    $connection = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
    die("Connection error: " . $e->getMessage());
}

$str = "XX\0XX";

header("Content-Type: text/plain");

print("Original: " . str_replace("\0", "{NUL}", $str) . "\n");
$str = $connection->quote($str);
print("Quoted:   " . str_replace("\0", "{NUL}", $str) . "\n");

?>

预期输出:

Original: XX{NUL}XX
Quoted:   'XX{NUL}XX'

实际输出:

Original: XX{NUL}XX
Quoted:   'XX'{NUL}{NUL}a

谁能解释这种奇怪的行为,更重要的是解释如何解决它?

更新

看起来最后一个字符是随机的,因为在随后的运行中它是一个e。这意味着某种形式的内存访问错误,例如读到字符串的末尾。可能是 pdo_sqlsrv 实现中的错误?

【问题讨论】:

标签: php sql-server pdo sqlsrv


【解决方案1】:

谁能解释这种奇怪的行为...

这似乎是 pdo_sqlsrv 扩展中的一个错误(并且很可能在非 PDO sqlsrv 扩展中的等效函数中)。

我在扩展程序的问题跟踪器 (ticket #538) 中记录了该问题,开发人员已确认他们可以复制该问题并且这是一个需要修复的错误。

...并且 - 更重要的是 - 解释如何解决它?

我们实施的解决方案是检测这种情况并重写字符串以删除任何 NUL 字符:

try {
    $dsn = 'sqlsrv:Server=.\SQLEXPRESS;Database=DBNAME';
    $user = 'USERNAME';
    $pass = 'PASSWORD';

    $connection = new PDO($dsn, $user, $pass);
} catch (PDOException $e) {
    die("Connection error: " . $e->getMessage());
}

$str = "XX\0XX";

header("Content-Type: text/plain");

print("Original: " . str_replace("\0", "{NUL}", $str) . "\n");
$str = safeQuote($str, $connection);
print("Quoted:   " . str_replace("\0", "{NUL}", $str) . "\n");

function safeQuote($str, $connection) {
// Special handling of ASCII NUL characters, which cause breakages.
// This appears to be due to a bug in the pdo_sqlsrv driver.
// For performance reasons, we only do this for strings that contain \0.
    if (is_string($str) && strpos($str, "\0") !== false) {
        $arrParts = explode("\0", $str);
        foreach ($arrParts as $Key => $Part) {
            $arrParts[$Key] = $connection->quote($Part);
        }
        $str = implode(" + CHAR(0) + ", $arrParts);
    }
// Otherwise, prepare the quoted string in the standard way.
    else {
        $str = $connection->quote($str);
    }

    return $str;
}

?>

这会输出以下内容,据我所知,这会导致在 SQL Server 中正确插入:

Original: XX{NUL}XX
Quoted:   'XX' + CHAR(0) + 'XX'


更新:2017 年 3 月

该错误已在SQLSRV/5.2.0 中修复。

【讨论】:

    【解决方案2】:

    在我的 PHP/5.6.21 下的 32 位 SQLSRV/3.2 设置中运行您的测试代码的略微修改版本显示出一种奇怪的行为:

    $str = "XX\0XX";
    for($i=0; $i<5; $i++){
        $connection = new PDO($dsn, $user, $pass);
        printf("0x%s -> 0x%s\n", bin2hex($str), bin2hex($connection->quote($str)));
        printf("0x%s -> 0x%s\n", bin2hex($str), bin2hex($connection->quote($str)));
    }
    
    0x5858005858 -> 0x27585827000005
    0x5858005858 -> 0x27585827000005
    0x5858005858 -> 0x2758582700b72b
    0x5858005858 -> 0x2758582700b72b
    0x5858005858 -> 0x27585827000306
    0x5858005858 -> 0x27585827000306
    0x5858005858 -> 0x2758582700b72b
    0x5858005858 -> 0x2758582700b72b
    0x5858005858 -> 0x27585827000005
    0x5858005858 -> 0x27585827000005
    

    每次运行时实际字节数都会发生变化。

    我的发现:

    • 结果在会话中是一致的,但在连接之间会随机变化
    • CharacterSet 连接选项(现在在这里显示)没有影响
    • github 存储库中有几个关于模拟准备的错误修复

    所以这一切都指向库中的一个错误。我已经尝试了最新的稳定版本(v4.3.0 在 32 位 PHP/7.1.9 下)并且它仍然存在(尽管,至少在我的系统上,结果仍然是任意的,但不是随机的)。

    无论是功能的错误,我想说PDO::quote() 在 SQLSRV 下不是二进制安全的。如果底层框架不允许准备好的语句,您可能需要考虑 converting to hex 并作为(未引用的)十六进制文字发送,例如:

    INSERT INTO foo (bar) VALUES (0x5858005858);
    

    【讨论】:

    • 您认为问题在于它不是二进制安全的,还是您认为这是 NUL 字符(通常在 Windows 应用程序中用作字符串终止符)的具体问题?你能想出一个我们可以测试的方法吗?
    • 不知道,抱歉。
    • 嗯,它已在扩展的较新版本中得到修复,所以这是一个没有实际意义的问题。我赞成这个答案,因为它是如何调查/证明问题的一个很好的例子,即使它没有提供直接的解决方案。
    【解决方案3】:
    1. 由于\ 可以在LIKE 语句中使用,您需要在引用之前添加斜杠,从而转义原始斜杠。

    2. 使用 PDO::quote 文档中提到的准备好的语句,而不是使用 PDO::quote。

    如果您使用此函数构建 SQL 语句,强烈建议您使用 PDO::prepare() 来准备带有绑定参数的 SQL 语句,而不是使用 PDO::quote() 将用户输入插入到 SQL 语句中.带有绑定参数的预处理语句不仅更便携、更方便、不受 SQL 注入的影响,而且执行起来通常比插值查询快得多,因为服务器端和客户端都可以缓存查询的编译形式。

    现场示例

    Sandbox

    输出

    <?php
    $pdo = new \PDO('sqlite::memory:', null, null);
    
    $str = "XX\0XX";
    
    print($pdo->quote(addSlashes($str))); <----> 'XX\0XX'
    
    ?>
    

    【讨论】:

    • str_replace() 仅用于提供调试输出,不是问题的一部分。根据我上面的评论,准备好的陈述与这个问题无关。我不认为你理解我的问题。
    • 谢谢,但这仍然行不通:SELECT CHARINDEX('\', 'XX\0XX'); 返回 3,这意味着 SQL Server 将 \0 视为两个字符 \ 和 0,而不是单个 NUL 字符. (请注意,对于国家字符串,您会得到相同的结果:SELECT CHARINDEX(N'\', N'XX\0XX');)。
    • @HappyDog 我会尝试做更多的研究,但我不太确定说实话,从来没有真正使用过PDO::quote,所以我从来没有遇到过这个问题。
    • 我很确定 PDO::quote() 是由驱动程序实现的,因此在 SQLite 中对其进行测试不太可能揭示 SQL Server 问题。
    猜你喜欢
    • 1970-01-01
    • 2015-04-22
    • 1970-01-01
    • 2018-09-03
    • 1970-01-01
    • 2011-01-27
    • 2011-02-01
    • 2019-12-13
    • 1970-01-01
    相关资源
    最近更新 更多