PDO - run unprepared statement containing "?" [duplicate] - php

For detecting the existence of a key in a hstore, I need to run a query like this:
SELECT * FROM tbl WHERE hst ? 'foo'
However, that gives me a PDOException:
PDOException: SQLSTATE[HY093]: Invalid parameter number: no parameters were bound: SELECT * FROM tbl WHERE hst ? 'foo'
Is there any way to escape the question mark so PDO won't pick it up as a placeholder? I've tried with up to four backslashes, as well as a double question mark (??), but nothing seems to persuade PDO to leave the question mark alone.

Use the function call form. According to the system catalogs, the hstore ? operator uses the exist function:
regress=# select oprname, oprcode from pg_operator where oprname = '?';
oprname | oprcode
---------+---------
? | exist
(1 row)
so you can write:
SELECT * FROM tbl WHERE exist(hst,'foo');
(Personally I'm not a big fan of hstore's operator-centric design and documentation, I think it discards the useful self-documenting properties of a function based interface without any real benefit and I usually use its function calls rather than its operators. Just because you can define operators doesn't mean you should.)

I had the same problem when searching on JSONB data. The full question is here
SELECT * FROM post WHERE locations ? :location;
The workaround on PostgreSQL 9.5 is similar:
SELECT * FROM post WHERE jsonb_exists(locations, :location);
I also opened a ticket at PHP bug tracing system
Update
As Diabl0 mentioned, the proposed solution work but does not use the index.
Tested with:
CREATE INDEX tempidxgin ON post USING GIN (locations);

I suggest you disable PDO native prepared statement so question marks will be ignored:
$pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true);

Related

Using PDO to retrieve an output parameter from a stored procedure in mysql

Before this is closed as a duplicate out of hand, let me say that there are a few questions that ask this on Stack Overflow, but none of them have marked answers, and the unmarked ones didn't work for me.
In my MySql database, I have a simple procedure:
DELIMITER //
CREATE PROCEDURE foo (
IN a INT,
OUT b INT
) BEGIN
SELECT a INTO b;
END //
DELIMITER ;
If I call this in phpMyAdmin, I get what I expect:
SELECT 2 INTO #b;
CALL foo(1, #b);
SELECT #b;
| #b |
+----+
| 1 |
When I try to call this from my php code, however, stmt->execute() returns false.
$a = 1;
$pdoLink = get_pdo_connection($db);
$stmt = $pdoLink->prepare('CALL `mydb`.`foo`(:a, :b)');
$stmt->bindParam(':a', $a);
$stmt->bindParam(':b', $b, PDO::PARAM_INT, 4);
// I've also tried length values of 1, 2, and 16
$stmt->execute(); // returns false
I thought maybe it had to do with calling a procedure in the first place, so I replaced my procedure with:
DELIMITER //
CREATE PROCEDURE foo (
IN a INT,
IN b INT
) BEGIN
SELECT a, b;
END //
DELIMITER ;
This, however, does work from php, so it must be something having to do with output parameters. I'm using code that's only slightly altered from the PDO Documentation, so I really have no idea what's wrong with it.
I am not looking for an answer that uses mysqli's multi_query to do something like
$sql = "CALL `mydb`.`foo`(1, #b);
SELECT #b;";
$link->multi_query($sql);
// ...
I'm looking for how to do this using PDO and a single query.
Someone tried to mark this as a duplicate of Calling stored procedure with Out parameter using PDO. The marked answer to this question uses multiple queries.
You say you used phpMyAdmin to test if your query works, but phpMyAdmin is built in PHP and uses mySQL Improved, so the problem is not on PHP.
Or better, to talk with MySQL you have to go through a driver that interprets the data between PHP and a system library; usually the libmysql, this interfaces with the MySQL server. It's all too complicated!
Many information on the web, say to set the PDO :: ATTR_EMULATE_PREPARES to false, to prevent the PHP emulates the preparation of the parameters passed to the query.
Other information, always taken from the web, they say to do it only if the PHP is compiled through the php5-mysqlnd module.
In practice I noticed that the best way for me was to cast the variables that carry non-string values.
So your code could become
...
$stmt->bindParam(':a', (int)$a);
$stmt->bindParam(':b', (int)$b, PDO::PARAM_INT, 4);
...
My two cents

How to ignore question mark as placeholder when using PDO with PostgreSQL

Note:
This question can be considered as duplicate of this Question.
It does point to the same problem with PDO. But its workaround solution is a bit different as the target differ. I will post there the workaround for JSONB and the link to the PHP ticket.
When I prepare the following query:
SELECT * FROM post WHERE locations ? :location;
The following warning occur:
Warning: PDO::prepare(): SQLSTATE[HY093]: Invalid parameter number: mixed named and positional parameters in /path/file.php on line xx
The question mark is an valid PostgreSQL operator but PDO condsider it as a placeholder.
Is there a proper way to configure PDO to ignore question mark as placeholders?
I will post a workaround bellow. Hoping there is a better way
Edit
I add a ticket at PHP bug tracing system
This is a workaround to my problem. It solve the problem by using PostgreSQL function to replace the ? operator.
I don't really like it because it does not make PDO more compliance to PostgreSQL. But I found no real solution.
CREATE FUNCTION json_key_exists(JSONB,TEXT) RETURNS BOOLEAN LANGUAGE SQL STABLE AS $f$
SELECT $1 ? $2
$f$;
And now I can use the query:
SELECT * FROM post WHERE json_key_exists(locations, :location);
The workaround was suggested by the fabulous RhodiumToad from freenode #postgresql
Edit
As #Abelisto suggested, there is no need to create the function above as jsonb_exists(jsonb, text) is avialabe
Since PHP 7.4, supports for escaping question mark have landed.
(...) question marks can be escaped by doubling them (...). That means that the “??” string would be translated to “?” when sending the query to the database, whereas “?” is still going to be interpreted as a positional parameter placeholder.
In your example, you can use:
$sql = "SELECT * FROM post WHERE locations ?? :location;";
Ok, the simplest way is to create the new operator with the same options, like:
-- Operator: ~!##%^&(jsonb, text)
-- DROP OPERATOR ~!##%^&(jsonb, text);
CREATE OPERATOR
~!##%^& -- Change it to any other non-conflicted symbols combination
(
PROCEDURE = jsonb_exists,
LEFTARG = jsonb,
RIGHTARG = text,
RESTRICT = contsel,
JOIN = contjoinsel);
COMMENT ON OPERATOR ~!##%^&(jsonb, text) IS 'key exists';
(Original script was generated by pgAdmin)
And use it in usual way like
SELECT * FROM post WHERE locations ~!##%^& :location;
You can use
jsonb_exists instead of ?
jsonb_exists_any instead of ?|
jsonb_exists_all instead of ?&
But there is no documentions on postgresql site.
For searching keys and according to Yoann answer, I have tested that the expression ( jsonbData ? 'keySearched' ) is equivalent to jsonb_exists(jsonbData , 'keySearched')
Use CREATE OPERATOR ~#& (LEFTARG = jsonb, RIGHTARG = text[], PROCEDURE = jsonb_exists_any) and use ~#& instead ?| everything will works fine
This works for me:
jsonb_exists(some_jsonb_array,'search_value');

SQL-injection with binding of an array to =ANY() condition

It assumed a more complex query with multiple bindings so please don't guide me to use the things like implode(',',$ids), (?,?,?) or PDO possibilities for this example.
The question is to clarify a possibility of the SQL-injection of this specific method.
There is parameter 1,2,3 in the url http://localhost/executeSql/1,2,3.
The parameter is passed by binding into = ANY operator as the string representation of the array '{1,2,3}' of PostgreSQL 9.3.
The php-code on Laravel 5.1:
public function executeSql($ids)
{
$ids='{'.$ids.'}';
$condition = 'WHERE id = ANY(:ids)';
$sql="SELECT id FROM (VALUES (1),(2),(3)) AS t(id) $condition";
DB::select($sql,[':ids'=>$ids]);
}
The result is the query:
SELECT id FROM (VALUES (1),(2),(3)) AS t(id) WHERE id = ANY('{1,2,3}')
That's works well untill the parameter contains integers only.
If the parameter is 1,2,3+ the QueryException occurs:
Invalid text representation: 7 ERROR: invalid input syntax for integer: "3+"
Can it be considered a proper protection to avoid SQL-injection?
As far as I understand from the documentation here and here , ANY convert the string you pass into an array and then use the operator (=) to compare each value in the array for one that would match.
In this case, I think pgsql do a little more: it has seen the lvalue (id) is of type integer, so it expect an array of integers. Since 3+ is not an integer, you have this one.
You should probably inspect the content of ids array (using filter_var and like) to ensure you have only integer values.
Since you definitively want the query to run with unintended result, this fails as a proper SQL injection because ANY checks its input and the query fails before running.
If however pgsql comes with a facility to build an array of integer from range, like {1:999999999999}, then you probably have a problem because the query will match a lot whole more rows.

Why MySQL fetches record even if it doesn't match the ID?

I just discovered this, I use MySQL as my database.
When i do something like:
$query = $this->db->where('id', '6rubbish')->get('posts', 1);
it generates and executes this SQL:
SELECT *
FROM (`posts`)
WHERE `id` = '6rubbish'
LIMIT 1
The surprising thing is that it actually fetches the post with the ID 6.
I find this very vulnerable in same cases because i'm trying to exactly match the ID, not to do a LIKE query.
Any ideas?
Yes.
Read Type Conversion in Expression Evaluation
Use intval() PHP function to extract the integer part of the variable
Or use is_int to exclude any variable that is not a pure integer
But the origin of the problem is that your query generator library doesn't understand the variable types, PHP being a dynamic typed language doesn't help too.
I don't know what library you're using, maybe there is an option to tell that you're passing an int? It should protect you from SQL injection, I hope, try with:
$query = $this->db->where('id', "don't")->get('posts', 1);
and see if the generated SQL has the single quoted escaped (doubled or preceded by backslash).
That is because SELECT 6 = '6rubbish' will give you true, and your id is number type.
Your id field is, no doubt, a numeric data type.
When MySQL evaluates expressions, it converts operands to compatible types (see docs).
'6rubbish' (a string) gets converted to 6 (a number) and, hence, you get a match.

"SELECT * FROM users WHERE id IN ( )" == FAIL

I have a function that I use called sqlf(), it emulates prepared statements. For instance I can do things like:
$sql = sqlf("SELECT * FROM Users WHERE name= :1 AND email= :2",'Big "John"','bj#example.com') ;
For various reasons, I cannot use prepared statements, but I would like to emulate them. The problem that I run into is with queries like
$sql = sqlf("SELECT * FROM Users WHERE id IN (:1)",array(1,2,3) );
My code works, but it fails with empty arrays, e.g. the following throws a mysql error:
SELECT * FROM Users WHERE id IN ();
Does anyone have any suggestions? How should I translate and empty array into sql that can be injected into an IN clause? Substituting NULL will not work.
Null is the only value that you can guarantee is not in the set. How come it is not an option? Anything else can be seen as part of the potential set, they are all values.
I would say that passing an empty array as argument for an IN() clause is an error. You have control over the syntax of the query when calling this function, so you should also be responsible for the inputs. I suggest checking for emptiness of the argument before calling the function.
Is there a possibility that you could detect empty arrays withing sqlf and change the SQL to not have the IN clause?
Alteratively, you could postprocess the SQL before passing it to the "real" SQL executor so that "IN ()" sections are removed although you'd have to do all sorts of trickery to see what other elements had to be removed so that:
SELECT * FROM Users WHERE id IN ();
SELECT * FROM Users WHERE a = 7 AND id IN ();
SELECT * FROM Users WHERE id IN () OR a = 9;
would become:
SELECT * FROM Users;
SELECT * FROM Users WHERE a = 7;
SELECT * FROM Users WHERE a = 9;
That could get tricky depending on the complexity of your SQL - you'd basically need a full SQL language interpreter.
If your prepare-like function simply replaces :1 with the equivalent argument, you might try having your query contain something like (':1'), so that if :1 is empty, it resolves to (''), which will not cause a parse error (however it may cause undesirable behavior, if that field can have blank values -- although if it's an int, this isn't a problem). It's not a very clean solution, however, and you're better off detecting whether the array is empty and simply using an alternate version of the query that lacks the "IN (:1)" component. (If that's the only logic in the WHERE clause, then presumably you don't want to select everything, so you would simply not execute the query.)
I would use zero, assuming your "id" column is a pseudokey that is assigned numbers automatically.
As far as I know, automatic key generators in most brands of database begin at 1. This is a convention, not a requirement (auto-numbered fields are not defined in standard SQL). But this convention is common enough that you can probably rely on it.
Since zero probably never appears in your "id" column, you can use this value in the IN() predicate when your input array is empty, and it'll never match.
The only way I can think to do it would be to make your sqlf() function scan to see if a particular substitution comes soon after an "IN (" and then if the passed variable is an empty array, put in something which you know for certain won't be in that column: "m,znmzcb~~1", for example. It's a hack, for sure but it would work.
If you wanted to take it even further, could you change your function so that there are different types of substitutions? It looks like your function scans for a colon followed by a number. Why not add another type, like an # followed by a number, which will be smart to empty arrays (this saves you from having to scan and guess if the variable is supposed to be an array).

Categories