Skip to content

Commit

Permalink
Merge pull request #48852 from f3ndot/issue-37779-escape-literal-colo…
Browse files Browse the repository at this point in the history
…ns-2

Allow escaping of literal colons in `ActionRecord::Sanitization#replace_named_bind_variables`
  • Loading branch information
eileencodes committed Aug 1, 2023
2 parents 35a614c + 694376f commit 98e12fc
Show file tree
Hide file tree
Showing 3 changed files with 18 additions and 2 deletions.
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
* Allow escaping of literal colon characters in `sanitize_sql_*` methods when named bind variables are used

*Justin Bull*

* Fix `#previously_new_record?` to return true for destroyed records.

Before, if a record was created and then destroyed, `#previously_new_record?` would return true.
Expand Down
11 changes: 9 additions & 2 deletions activerecord/lib/active_record/sanitization.rb
Expand Up @@ -137,14 +137,19 @@ def sanitize_sql_like(string, escape_character = "\\")
end

# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
# sanitized and interpolated into the SQL statement. If using named bind
# variables in SQL statements where a colon is required verbatim use a
# backslash to escape.
#
# sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"
#
# sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
# # => "name='foo''bar' and group_id=4"
#
# sanitize_sql_array(["TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12\\:MI\\:SS')", date: "foo"])
# # => "TO_TIMESTAMP('foo', 'YYYY/MM/DD HH12:MI:SS')"
#
# sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
# # => "name='foo''bar' and group_id='4'"
#
Expand Down Expand Up @@ -206,9 +211,11 @@ def replace_bind_variable(value, c = connection)
end

def replace_named_bind_variables(statement, bind_vars)
statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
statement.gsub(/([:\\]?):([a-zA-Z]\w*)/) do |match|
if $1 == ":" # skip postgresql casts
match # return the whole match
elsif $1 == "\\" # escaped literal colon
match[1..-1] # return match with escaping backlash char removed
elsif bind_vars.include?(match = $2.to_sym)
replace_bind_variable(bind_vars[match])
else
Expand Down
5 changes: 5 additions & 0 deletions activerecord/test/cases/sanitize_test.rb
Expand Up @@ -224,6 +224,11 @@ def test_named_bind_with_postgresql_type_casts
assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end

def test_named_bind_with_literal_colons
assert_equal "TO_TIMESTAMP('2017/08/02 10:59:00', 'YYYY/MM/DD HH12:MI:SS')", bind("TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12\\:MI\\:SS')", date: "2017/08/02 10:59:00")
assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "TO_TIMESTAMP(:date, 'YYYY/MM/DD HH12:MI:SS')", date: "2017/08/02 10:59:00" }
end

private
def bind(statement, *vars)
if vars.first.is_a?(Hash)
Expand Down

0 comments on commit 98e12fc

Please sign in to comment.