Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow escaping of literal colons in ActionRecord::Sanitization#replace_named_bind_variables #48852

Merged
merged 1 commit into from Aug 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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