Suggestion: PG::Connection#complile#726
Conversation
2cd9444 to
b0a2666
Compare
b0a2666 to
6b746e9
Compare
|
That sounds like a great idea! It's because I manually inserted too many parameters into too many queries from rails debug log in order to execute it in |
.. to replace code specific to the classes derived from PG::TypeMap. Also: - Deny binary format in SQL text - Make BinaryString working
|
I added a commit to use generic PG::TypeMap methods only and regarding binary data and binary format. The method name "compile" is not very specific. Is it used in any other database driver? How sounds |
|
@larskanis thank you. |
I don't have any strong preferences regarding this. |
This adds type casts like "::bytea" to the escaped and embedded values. To make that possible the OID => type name mapping is needed. It can be given as explicit CoderMapsBundle, is retrieved from BasicTypeMap* or retrieved from the database. Also allow `type_map` to be provided like `exec_params` allows. This patch changes `compile` to `embed_params` to be more specific. This patch changes the tests to compare `exec(embed_params())` to `exec_params()`. That ensures the processing by the database server is the same. Since binary format encoders are not usable for embedding into SQL text, this patch changes boolean encoders to text. There's no value in using a binary encoder for query parameters. It's not measurable faster than text for boolean type.
…rings This issue was raised by a rails test: test/cases/adapters/postgresql/bytea_test.rb:114
|
🎉 With the latest change the whole activerecord test suite runs through with only 2 expected errors, when I change the following AR lines to got through |
|
@larskanis thank you for your effort and covering cases I was not aware of. |
| # Compiles your prepared SQL statement and the given positional arguments into plain SQL string. | ||
| # | ||
| # The resulting SQL string can be used with +conn.exec+ like the prepared SQL statement and parameters with +conn.exec_params+. | ||
| # +conn.exec_params+ is usually preferred because it's faster and safer. |
There was a problem hiding this comment.
IME, these kinds of not-quite-parsers are vulnerable to SQL injection.
Perhaps a stronger or more specific statement about how this should/not be used with user input.
There was a problem hiding this comment.
I agree. If there is any mistake in regexp - the result may end up containing sql injection. For example
embed_params("select '$1' as one, $1", [%{', 2 as two, }])may result in
[{"?column?" => "', 2 as two, ", "two" => "2", "one" => ""}]instead this
[{"one" => "$1", "?column?" => "', 2 as two, "}]if we forget to cover regular string literal(e.g. 'my str') in PLACEHOLDER_RE. Thus, as long as we cover all possible variations of string literals and escape the value - we are safe. Fortunately I covered all possible scenarios in tests. As an additional measure - I can put here a guard that fails on new postgresql versions and a dev should check if any new string literals appear in a new postgresql version and adjust the implementation accordingly. Something like this(in #embed_params_and_check):
# Query returns a string like "18.4 (Ubuntu 18.4-0ubuntu0.26.04.1)"
current_pg_version = conn.exec('SHOW server_version').first['server_version'].split(' ').first
current_pg_version = Gem::Version.new(current_pg_version)
expect(current_pg_version).to be < Gem::Version.new('19'), "PostgreSQL 19 is not tested. Please ensure there are no new forms of a string literal and increase the version to make this test pass."What do you think, guys?
There was a problem hiding this comment.
Yes, maybe we should make a stronger statement about security. Putting SQL and user data together into one string is always more risky than keeping them separate.
On the other hand to exploit a incomplete or broken regex it's necessary to have a strange SQL placeholder string. But this string must not contain any unchecked user inputs anyway, so cannot be manipulated. And the params items go through the escape or escape_bytea methods provided by libpq, so that they should be safe.
So I think it's not necessary to put a guard for new PostgreSQL versions. But maybe someone speaking native English can improve my wording or make it more specific.
There was a problem hiding this comment.
Unfortunately #escape does not fully protect us from sql injections. This is because we have to support every possible string literals here, but #escape only ensures there is correct amount of ' in the string. Thus, if some regexp contains a mistake - an injection becomes possible for other string literals. In the example bellow you can see an injection using $$ in case this variant is not handled properly(you can simply delete that variant to reproduce the issue):
sql = "select $$ $1 as t, $$ as one, $1"
params = ["$$ as tt, $$"]
@conn.exec(@conn.embed_params(sql, params)).to_a
# => [{"tt" => " '", "one" => "' as t, ", "?column?" => "$$ as tt, $$"}]
@conn.exec_params(sql, params).to_a
# [{"one" => " $1 as t, ", "?column?" => "$$ as tt, $$"}]Thus, such issue might appear because we basically replace all appearances of positional variable in the string thanks to gsub and incorrect/absent regexp. This is what I meant in my previous message when I was suggesting to add another guard against new potential string literals in future PostgreSQL versions.
There was a problem hiding this comment.
@intale I understand your concern. But what I mean is that the SQL string must be manipulated, in order to exploit a flaw in the regex, which should not be possible for an attacker. An attacker can change the params, but he cannot exploit a flaw in the regex.
There was a problem hiding this comment.
Thanks for explanation. That is true. Let me rephrase my concern. My intention of adding a guard is to prevent next case:
- current implementation covers all cases for current PostreSQL
- PosteeSQL v25 introduces new string literal. Let's say
%% my str %% - the implementation suddenly becomes vulnerable to sql injections if new string literal is used because it misses a regexp to handle it
But if we would have a guard that demands manual check of new PostreSQL version support before succeeding the build - we would be more safe than without it.
Introduce
PG::Connection#complilethat take sql string and position parameters and compiles it into a plain sql. I couldn't find another way how to do it using built-in tools. The motivation behind this implementation:The downside of this implementation is that it can't reliably encode binary data into a string representation except some common cases like with booleans, but do we need them at all?