From b19fdc18a974929394c734d8b710d7a9ca3c9d3a Mon Sep 17 00:00:00 2001 From: Matteo Beccati Date: Mon, 22 Jul 2019 19:22:07 +0200 Subject: [PATCH] Fix FR #71885 (Allow escaping question mark placeholders) --- ext/pdo/pdo_sql_parser.re | 75 +++++++++++++++++++++-------- ext/pdo/tests/bug_71885.phpt | 54 +++++++++++++++++++++ ext/pdo_pgsql/pgsql_driver.c | 46 +++++++++--------- ext/pdo_pgsql/tests/bug71885.phpt | 46 ++++++++++++++++++ ext/pdo_pgsql/tests/bug71885_2.phpt | 57 ++++++++++++++++++++++ 5 files changed, 236 insertions(+), 42 deletions(-) create mode 100644 ext/pdo/tests/bug_71885.phpt create mode 100644 ext/pdo_pgsql/tests/bug71885.phpt create mode 100644 ext/pdo_pgsql/tests/bug71885_2.phpt diff --git a/ext/pdo/pdo_sql_parser.re b/ext/pdo/pdo_sql_parser.re index b04e2fb928b7d..71d41002cf0e0 100644 --- a/ext/pdo/pdo_sql_parser.re +++ b/ext/pdo/pdo_sql_parser.re @@ -23,7 +23,10 @@ #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 index 0000000000000..64e92608be20f --- /dev/null +++ b/ext/pdo/tests/bug_71885.phpt @@ -0,0 +1,54 @@ +--TEST-- +PDO Common: FR #71885 (Allow escaping question mark placeholders) +--SKIPIF-- + +--FILE-- +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=== diff --git a/ext/pdo_pgsql/pgsql_driver.c b/ext/pdo_pgsql/pgsql_driver.c index 15b2bd16c9001..9c5df24ab5324 100644 --- a/ext/pdo_pgsql/pgsql_driver.c +++ b/ext/pdo_pgsql/pgsql_driver.c @@ -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 index 0000000000000..ea5b1882ffaef --- /dev/null +++ b/ext/pdo_pgsql/tests/bug71885.phpt @@ -0,0 +1,46 @@ +--TEST-- +Request #71855 (PDO placeholder escaping) +--SKIPIF-- + +--FILE-- +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 index 0000000000000..f98e9ef785a95 --- /dev/null +++ b/ext/pdo_pgsql/tests/bug71885_2.phpt @@ -0,0 +1,57 @@ +--TEST-- +Request #71855 (PDO placeholder escaping, part 2) +--SKIPIF-- +getAttribute(PDO::ATTR_SERVER_VERSION), '9.4.0') < 0) { + die("skip Requires 9.4+"); +} + +?> +--FILE-- +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==