0

I want a way to show SQL query how it will look with anonymous parameters (?) substituted by actual parameters.
This is only for readability purposes and debugging, and wouldn't be used as an actual query.

I have found this function which works for the most part:

return array_reduce($this->bindValues, function ($sql, $binding) {
  return preg_replace('/\?/', is_numeric($binding) ? $binding : '"' . $binding . '"', $sql, 1);
}, $this->query);

replacing ? with actual values:

    $data = array(
        'item' => '1,
        'type' => 'Are you ok.'
    );
UPDATE `list` set `item`=?,`type`=? WHERE  (`id` = ?) ;
UPDATE `list` set `item`="1",`type`="Are you ok." WHERE (`id` = 1) ;

but if the value contains ? I'm ending up with:

    $data = array(
        'item' => '1,
        'type' => 'Are you ok?'
    );
UPDATE `list` set `item`="1",`type`="Are you ok2" WHERE (`id` = ?) ;

How can I make this work, so only binding ? are replaced.

ADyson
  • 57,178
  • 14
  • 51
  • 63
Mike
  • 19
  • 2
  • 1
    Let me reiterate, you took safe, recommended on every corner approach and want to convert it to allow SQL injections? – markalex Apr 16 '23 at 09:23
  • 1
    Are you just using this to try and log/debug the SQL statements being executed? I really hope so. Otherwise I can't begin to imagine why you'd want to convert a correctly parameterised statement back into an un-parameterised one. – ADyson Apr 16 '23 at 09:24
  • 1
    Of course it's for readability purposes and debugging – Mike Apr 16 '23 at 09:28
  • 1
    No problem, just checking...as you're new you probably don't appreciate the volume of insecure SQL code, and related misunderstandings and mistakes, that we see on here on a daily basis. It would, sadly, not be surprising to many of us if someone was engaged in actively making their code less secure, because they saw (and probably misinterpreted) some nonsense on youtube or something. Sounds like that's not the case here, fortunately. – ADyson Apr 16 '23 at 09:29
  • P.S. While not directly about your specific preg-replace issue, you may be interested to read https://stackoverflow.com/questions/2411182/how-to-debug-pdo-database-queries – ADyson Apr 16 '23 at 09:31
  • Anyway it seems like the aim of your regex should be to only replace `?`s which aren't inside quoted strings. I'm far from a regex expert so not sure how best you could do that. – ADyson Apr 16 '23 at 09:33
  • 1
    @ADyson, regexes are not really great at detecting inside/outside. I believe PDO [supports named parameters](https://www.php.net/manual/en/pdo.prepare.php). Wouldn't it be easier just to use those? – markalex Apr 16 '23 at 09:37
  • @markalex sounds like a good idea. Of course the OP may have large codebase which could be time-consuming to convert. Let's see if we get any feedback on that. – ADyson Apr 16 '23 at 09:41
  • And if named parameters are of the table, you'll need to do this in a couple of steps: 1. prepare your parameters by replacing `?` with `\?`, 2. Substitute parameters using regex `(?<!\\)?`, 3. Substitute all `\?` with `?`. Or something of a similar manner. – markalex Apr 16 '23 at 09:44
  • I have ended up using simple str_replace("?", "*", $query), on raw query, and afterwards just regex to replace * with actual values. – Mike Apr 16 '23 at 10:21
  • If you have a solution please write an Answer below - see also [answer]...thanks :-) – ADyson Apr 16 '23 at 11:46
  • 1
    @Mike That would cause the same problem with strings that include `*` though won't it? In addition `*` can be found in things like `SELECT * FROM table WHERE id=?` – apokryfos Apr 16 '23 at 12:30
  • No matter what you temporarily replace substrings with, there is a risk of damaging the SQL. Try a SQL parser. – mickmackusa Apr 16 '23 at 21:26
  • @mickmackusa There is no damage to the original raw query, i'm only manipulating a replica stored in another variable. So this way I can actually debug and see the real values – Mike Apr 24 '23 at 07:32
  • 1
    Yes, I understand your task. I am saying that your outcome for the debugging version of the query will be at risk of being broken. The point of doing this task is so that the diagnostic output is clear (unbroken). – mickmackusa Apr 24 '23 at 08:40

2 Answers2

0

First of all, consider using named parameters instead of ?. In this case you'll don't need to substitute anything: named parameters are clear and fairly easy to show in logs, and supported by most dbms clients for debugging purposes.

If named parameters are not feasible (due to large current code base or any other reason) you have two major ways:

  1. Use SQL query parser. It will produce most reliable outcome.
  2. Use some kind of self written "substitutor". It's outcome never will be ideal or fully reliable, but should be fast both in terms of performance and development.

If you'll choose latter way, here is example how it could be done quick and dirty:

Do your substitution in multiple steps:

  1. Prepare your parameters by replacing ? with something other, highly unlikely to be present in parameters or query. For example \?.
  2. Substitute parameters using regex, that will match ?, but would not match substitution from first step. For substitution with \? it will be (?<!\\)?
  3. Substitute all \? with ? in result.

ATTENTION: result of this substitution should never be used as a query in your program. Such a substitution has a potential for any or all of the below:

  • SQL injections,
  • inaccurate result if initial query contains ? not as a parameter (for example in comments),
  • inaccurate result if initial query or any of the parameters contain substitution string (\? in our example).
<?php
$query = 'UPDATE `list` set `item`=?,`type`=? WHERE  (`id` = ?);';
$params = array(
    'item' => '1',
    'type' => 'Are you o\'k?',
    'id'   => 2
);

function substitute_params($query, $params) {
    $prep_params =  str_replace(array("'","?"),array("''","\\?"),$params);
    $query = array_reduce($prep_params, function ($interm, $param) {
        return preg_replace('/(?<!\\\\)\\?/m', 
            is_numeric($param) ? $param : '\'' . $param . '\'', 
            $interm, 
            1);
    }, $query);
    return "-- Not to be used as a query to database. For demonstration purposes only!\n"
      .str_replace("\\?", "?", $query);
}
echo substitute_params($query, $params);
?>

Outputs:

-- Not to be used as a query to database. For demonstration purposes only!
UPDATE `list` set `item`=1,`type`='Are you o''k?' WHERE  (`id` = 2);

EDIT: to try and lower impact of question marks inside of constant strings and quoted names you could try to use this replacement:

        return preg_replace('/^([^"\'`]*?(?:(?:`[^`]*?`[^"\'`]*?)*?(?:"[^"]*?"[^"\'`]*?)*?(?:\'[^\']*?\'[^\'"`]*?)*?)*?)(?<!\\\\)\\?/m', 
            '${1}'.(is_numeric($param) ? $param : '\'' . $param . '\''), 
            $interm, 
            1);

It substitutes only ? outside of blocks quoted with either of "`'.

Demo can be seen here.

Please remember, this is not fully fledged parser. It is not aware of comments, for example. So erroneous substitutions are still very much possible.

markalex
  • 8,623
  • 2
  • 7
  • 32
  • How about `$query = 'UPDATE `list` set `item`=?,`type`=? WHERE `item` != 'Are you okay?' AND `id` = ?';` See how a SQL parser would be a more ideal approach? Otherwise, you are replacing substings that _look_ like placeholders. This is the same trap as when using regex to parse HTML. – mickmackusa Apr 16 '23 at 22:21
  • @mickmackusa, regarding parser: I personally believe that for stated objective parser is overkill. In general moving to named parameters would be way better spent time. But I added note about use of parser to answer, not to be one-sided. It would be great if you propose solution based on parser, or at least put a related link into questions comment, as it's obviously out of my reach with my php level. – markalex Apr 16 '23 at 22:43
  • @mickmackusa, `1e1` is valid number in Oracle, MySQL and Postgre, but I'm not sure if it is guarantied by ASNI though. I believe `is_numeric()` is "good enough" for task at hand. – markalex Apr 16 '23 at 22:45
  • A different styling of this answer: https://3v4l.org/KWFqF And here is a monkey wrenching sample query: https://3v4l.org/VZtD2 – mickmackusa Apr 16 '23 at 23:46
  • @mickmackusa, added solution for `?` in quoted strings. – markalex Apr 17 '23 at 07:31
-1

You can always use plain string operations:

<?php

$str = 'UPDATE `list` set `item`=?,`type`=? WHERE  (`id` = ?);';

$data = array(
    'item' => '1',
    'type' => 'Are you ok?'
);

$pos = 0;
$index = 0;
do {
    $pos = strpos($str, '?', $pos);
    if ($pos === false) {
        break;
    }
    $binding = array_values($data)[$index%count($data)];
    $replacement = is_numeric($binding) ? $binding : '"' . $binding . '"';
    $str = substr_replace($str, $replacement, $pos, 1);
    $pos += strlen($replacement);
    $index++;
} while ($pos !== false);

echo $str; // Outputs UPDATE `list` set `item`=1,`type`="Are you ok?" WHERE  (`id` = 1);

3v4l code

apokryfos
  • 38,771
  • 9
  • 70
  • 114
  • I don't know who downvoted you, but since your answer does what it is expected, most likely its about principle. But you could at least add disclaimer that it wouldn't works if there are ? in strings in query, and shouldn't be used instead of parameterized queries . – markalex Apr 16 '23 at 21:19
  • What if I have an column alias in my SQL called `active?`? What if my SQL has a hardcoded string value containing a `?`? – mickmackusa Apr 16 '23 at 21:21