]> granicus.if.org Git - php/commitdiff
Fix FR #71885 (Allow escaping question mark placeholders)
authorMatteo Beccati <mbeccati@php.net>
Mon, 22 Jul 2019 17:22:07 +0000 (19:22 +0200)
committerMatteo Beccati <mbeccati@php.net>
Mon, 22 Jul 2019 17:35:03 +0000 (19:35 +0200)
ext/pdo/pdo_sql_parser.re
ext/pdo/tests/bug_71885.phpt [new file with mode: 0644]
ext/pdo_pgsql/pgsql_driver.c
ext/pdo_pgsql/tests/bug71885.phpt [new file with mode: 0644]
ext/pdo_pgsql/tests/bug71885_2.phpt [new file with mode: 0644]

index b04e2fb928b7d006ae6badb4efb01e5f17d078f1..71d41002cf0e03d9179340faddac88e6a5c65985 100644 (file)
 #define PDO_PARSER_TEXT 1
 #define PDO_PARSER_BIND 2
 #define PDO_PARSER_BIND_POS 3
-#define PDO_PARSER_EOI 4
+#define PDO_PARSER_ESCAPED_QUESTION 4
+#define PDO_PARSER_EOI 5
+
+#define PDO_PARSER_BINDNO_ESCAPED_CHAR -1
 
 #define RET(i) {s->cur = cursor; return i; }
 #define SKIP_ONE(i) {s->cur = s->tok + 1; return i; }
@@ -46,9 +49,10 @@ static int scan(Scanner *s)
        /*!re2c
        BINDCHR         = [:][a-zA-Z0-9_]+;
        QUESTION        = [?];
+       ESCQUESTION     = [?][?];
        COMMENTS        = ("/*"([^*]+|[*]+[^/*])*[*]*"*/"|"--"[^\r\n]*);
        SPECIALS        = [:?"'-/];
-       MULTICHAR       = ([:]{2,}|[?]{2,});
+       MULTICHAR       = [:]{2,};
        ANYNOEOF        = [\001-\377];
        */
 
@@ -56,6 +60,7 @@ static int scan(Scanner *s)
                (["](([\\]ANYNOEOF)|ANYNOEOF\["\\])*["]) { RET(PDO_PARSER_TEXT); }
                (['](([\\]ANYNOEOF)|ANYNOEOF\['\\])*[']) { RET(PDO_PARSER_TEXT); }
                MULTICHAR                                                               { RET(PDO_PARSER_TEXT); }
+               ESCQUESTION                                                             { RET(PDO_PARSER_ESCAPED_QUESTION); }
                BINDCHR                                                                 { RET(PDO_PARSER_BIND); }
                QUESTION                                                                { RET(PDO_PARSER_BIND_POS); }
                SPECIALS                                                                { SKIP_ONE(PDO_PARSER_TEXT); }
@@ -85,7 +90,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
        char *ptr, *newbuffer;
        ptrdiff_t t;
        uint32_t bindno = 0;
-       int ret = 0;
+       int ret = 0, escapes = 0;
        size_t newbuffer_len;
        HashTable *params;
        struct pdo_bound_param_data *param;
@@ -98,14 +103,19 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
 
        /* phase 1: look for args */
        while((t = scan(&s)) != PDO_PARSER_EOI) {
-               if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS) {
+               if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS || t == PDO_PARSER_ESCAPED_QUESTION) {
+                       if (t == PDO_PARSER_ESCAPED_QUESTION && stmt->supports_placeholders == PDO_PLACEHOLDER_POSITIONAL) {
+                               /* escaped question marks unsupported, treat as text */
+                               continue;
+                       }
+
                        if (t == PDO_PARSER_BIND) {
                                ptrdiff_t len = s.cur - s.tok;
                                if ((inquery < (s.cur - len)) && isalnum(*(s.cur - len - 1))) {
                                        continue;
                                }
                                query_type |= PDO_PLACEHOLDER_NAMED;
-                       } else {
+                       } else if (t == PDO_PARSER_BIND_POS) {
                                query_type |= PDO_PLACEHOLDER_POSITIONAL;
                        }
 
@@ -114,7 +124,16 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
                        plc->next = NULL;
                        plc->pos = s.tok;
                        plc->len = s.cur - s.tok;
-                       plc->bindno = bindno++;
+
+                       if (t == PDO_PARSER_ESCAPED_QUESTION) {
+                               plc->bindno = PDO_PARSER_BINDNO_ESCAPED_CHAR;
+                               plc->quoted = "?";
+                               plc->qlen = 1;
+                               plc->freeq = 0;
+                               escapes++;
+                       } else {
+                               plc->bindno = bindno++;
+                       }
 
                        if (placetail) {
                                placetail->next = plc;
@@ -125,7 +144,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
                }
        }
 
-       if (bindno == 0) {
+       if (!placeholders) {
                /* nothing to do; good! */
                return 0;
        }
@@ -140,11 +159,16 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
 
        if (stmt->supports_placeholders == query_type && !stmt->named_rewrite_template) {
                /* query matches native syntax */
+               if (escapes) {
+                       newbuffer_len = inquery_len;
+                       goto rewrite;
+               }
+
                ret = 0;
                goto clean_up;
        }
 
-       if (stmt->named_rewrite_template) {
+       if (query_type == PDO_PLACEHOLDER_NAMED && stmt->named_rewrite_template) {
                /* magic/hack.
                 * We we pretend that the query was positional even if
                 * it was named so that we fall into the
@@ -155,14 +179,7 @@ PDO_API int pdo_parse_params(pdo_stmt_t *stmt, char *inquery, size_t inquery_len
 
        params = stmt->bound_params;
 
-       /* Do we have placeholders but no bound params */
-       if (bindno && !params && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) {
-               pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "no parameters were bound");
-               ret = -1;
-               goto clean_up;
-       }
-
-       if (params && bindno != zend_hash_num_elements(params) && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) {
+       if (bindno && stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && params && bindno != zend_hash_num_elements(params)) {
                /* extra bit of validation for instances when same params are bound more than once */
                if (query_type != PDO_PLACEHOLDER_POSITIONAL && bindno > zend_hash_num_elements(params)) {
                        int ok = 1;
@@ -188,7 +205,16 @@ safe:
                newbuffer_len = inquery_len;
 
                /* let's quote all the values */
-               for (plc = placeholders; plc; plc = plc->next) {
+               for (plc = placeholders; plc && params; plc = plc->next) {
+                       if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
+                               /* escaped character */
+                               continue;
+                       }
+
+                       if (query_type == PDO_PLACEHOLDER_NONE) {
+                               continue;
+                       }
+
                        if (query_type == PDO_PLACEHOLDER_POSITIONAL) {
                                param = zend_hash_index_find_ptr(params, plc->bindno);
                        } else {
@@ -316,8 +342,13 @@ rewrite:
                                memcpy(newbuffer, ptr, t);
                                newbuffer += t;
                        }
-                       memcpy(newbuffer, plc->quoted, plc->qlen);
-                       newbuffer += plc->qlen;
+                       if (plc->quoted) {
+                               memcpy(newbuffer, plc->quoted, plc->qlen);
+                               newbuffer += plc->qlen;
+                       } else {
+                               memcpy(newbuffer, plc->pos, plc->len);
+                               newbuffer += plc->len;
+                       }
                        ptr = plc->pos + plc->len;
 
                        plc = plc->next;
@@ -350,6 +381,11 @@ rewrite:
                for (plc = placeholders; plc; plc = plc->next) {
                        int skip_map = 0;
                        char *p;
+
+                       if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) {
+                               continue;
+                       }
+
                        name = estrndup(plc->pos, plc->len);
 
                        /* check if bound parameter is already available */
@@ -395,6 +431,7 @@ rewrite:
                        efree(name);
                        plc->quoted = "?";
                        plc->qlen = 1;
+                       newbuffer_len -= plc->len - 1;
                }
 
                goto rewrite;
diff --git a/ext/pdo/tests/bug_71885.phpt b/ext/pdo/tests/bug_71885.phpt
new file mode 100644 (file)
index 0000000..64e9260
--- /dev/null
@@ -0,0 +1,54 @@
+--TEST--
+PDO Common: FR #71885 (Allow escaping question mark placeholders)
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo')) die('skip');
+$dir = getenv('REDIR_TEST_DIR');
+if (false == $dir) die('skip no driver');
+if (!strncasecmp(getenv('PDOTEST_DSN'), 'pgsql', strlen('pgsql'))) die('skip not relevant for pgsql driver');
+if (!strncasecmp(getenv('PDOTEST_DSN'), 'odbc', strlen('odbc'))) die('skip inconsistent error message with odbc');
+require_once $dir . 'pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+if (getenv('REDIR_TEST_DIR') === false) putenv('REDIR_TEST_DIR='.dirname(__FILE__) . '/../../pdo/tests/');
+require_once getenv('REDIR_TEST_DIR') . 'pdo_test.inc';
+$db = PDOTest::factory();
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+$db->exec("CREATE TABLE test (a int)");
+
+$sql = "SELECT * FROM test WHERE a ?? 1";
+
+try {
+       $db->exec($sql);
+} catch (PDOException $e) {
+       var_dump(strpos($e->getMessage(), "?") !== false);
+}
+
+try {
+       $stmt = $db->prepare($sql);
+       $stmt->execute();
+} catch (PDOException $e) {
+       var_dump(strpos($e->getMessage(), "?") !== false);
+}
+
+if ($db->getAttribute(PDO::ATTR_DRIVER_NAME) == 'mysql') {
+       $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, 1);
+}
+
+try {
+       $stmt = $db->prepare($sql);
+       $stmt->execute();
+} catch (PDOException $e) {
+       var_dump(strpos($e->getMessage(), "?") !== false);
+}
+
+?>
+===DONE===
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+===DONE===
index 15b2bd16c9001769958774b232c859fdf18d8834..9c5df24ab53249cb9a0531d744dd927d5f1b24f0 100644 (file)
@@ -256,36 +256,36 @@ static int pgsql_handle_preparer(pdo_dbh_t *dbh, const char *sql, size_t sql_len
                execute_only = H->disable_prepares;
        }
 
-       if (!emulate && PQprotocolVersion(H->server) > 2) {
+       if (!emulate && PQprotocolVersion(H->server) <= 2) {
+               emulate = 1;
+       }
+
+       if (emulate) {
+               stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
+       } else {
                stmt->supports_placeholders = PDO_PLACEHOLDER_NAMED;
                stmt->named_rewrite_template = "$%d";
-               ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);
-
-               if (ret == 1) {
-                       /* query was re-written */
-                       sql = nsql;
-               } else if (ret == -1) {
-                       /* couldn't grok it */
-                       strcpy(dbh->error_code, stmt->error_code);
-                       return 0;
-               }
+       }
 
-               if (!execute_only) {
-                       /* prepared query: set the query name and defer the
-                          actual prepare until the first execute call */
-                       spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
-               }
+       ret = pdo_parse_params(stmt, (char*)sql, sql_len, &nsql, &nsql_len);
 
-               if (nsql) {
-                       S->query = nsql;
-               } else {
-                       S->query = estrdup(sql);
-               }
+       if (ret == -1) {
+               /* couldn't grok it */
+               strcpy(dbh->error_code, stmt->error_code);
+               return 0;
+       } else if (ret == 1) {
+               /* query was re-written */
+               S->query = nsql;
+       } else {
+               S->query = estrdup(sql);
+       }
 
-               return 1;
+       if (!emulate && !execute_only) {
+               /* prepared query: set the query name and defer the
+                  actual prepare until the first execute call */
+               spprintf(&S->stmt_name, 0, "pdo_stmt_%08x", ++H->stmt_counter);
        }
 
-       stmt->supports_placeholders = PDO_PLACEHOLDER_NONE;
        return 1;
 }
 
diff --git a/ext/pdo_pgsql/tests/bug71885.phpt b/ext/pdo_pgsql/tests/bug71885.phpt
new file mode 100644 (file)
index 0000000..ea5b188
--- /dev/null
@@ -0,0 +1,46 @@
+--TEST--
+Request #71855 (PDO placeholder escaping)
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
+require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
+require_once dirname(__FILE__) . '/config.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
+require_once dirname(__FILE__) . '/config.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM);
+
+foreach ([false, true] as $emulate) {
+       $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
+
+       try {
+               $stmt = $db->prepare('select ?- lseg \'((-1,0),(1,0))\'');
+               $stmt->execute();
+       } catch (PDOException $e) {
+               var_dump('ERR');
+       }
+
+       $stmt = $db->prepare('select ??- lseg \'((-1,0),(1,0))\'');
+       $stmt->execute();
+
+       var_dump($stmt->fetch());
+}
+
+?>
+==OK==
+--EXPECT--
+string(3) "ERR"
+array(1) {
+  [0]=>
+  bool(true)
+}
+array(1) {
+  [0]=>
+  bool(true)
+}
+==OK==
diff --git a/ext/pdo_pgsql/tests/bug71885_2.phpt b/ext/pdo_pgsql/tests/bug71885_2.phpt
new file mode 100644 (file)
index 0000000..f98e9ef
--- /dev/null
@@ -0,0 +1,57 @@
+--TEST--
+Request #71855 (PDO placeholder escaping, part 2)
+--SKIPIF--
+<?php
+if (!extension_loaded('pdo') || !extension_loaded('pdo_pgsql')) die('skip not loaded');
+require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
+require_once dirname(__FILE__) . '/config.inc';
+PDOTest::skip();
+
+$db = PDOTest::factory();
+if (version_compare($db->getAttribute(PDO::ATTR_SERVER_VERSION), '9.4.0') < 0) {
+        die("skip Requires 9.4+");
+}
+
+?>
+--FILE--
+<?php
+require_once dirname(__FILE__) . '/../../../ext/pdo/tests/pdo_test.inc';
+require_once dirname(__FILE__) . '/config.inc';
+$db = PDOTest::test_factory(dirname(__FILE__) . '/common.phpt');
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_NUM);
+
+$jsonb = $db->quote(json_encode(['a' => 1]));
+
+foreach ([false, true] as $emulate) {
+       $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
+
+       $stmt = $db->prepare("SELECT {$jsonb}::jsonb ?? ?");
+       $stmt->execute(['b']);
+       var_dump($stmt->fetch());
+
+       $stmt = $db->prepare("SELECT {$jsonb}::jsonb ???");
+       $stmt->execute(['a']);
+       var_dump($stmt->fetch());
+}
+
+?>
+==OK==
+--EXPECT--
+array(1) {
+  [0]=>
+  bool(false)
+}
+array(1) {
+  [0]=>
+  bool(true)
+}
+array(1) {
+  [0]=>
+  bool(false)
+}
+array(1) {
+  [0]=>
+  bool(true)
+}
+==OK==