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

Update tests and docs for ActiveRecord::Sanitization #44429

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
27 changes: 14 additions & 13 deletions activerecord/lib/active_record/sanitization.rb
Expand Up @@ -5,14 +5,14 @@ module Sanitization
extend ActiveSupport::Concern

module ClassMethods
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
# Accepts an array or string of SQL conditions and sanitizes them into
# a valid SQL fragment for a WHERE clause, using the adapter of the current `connection`.
#
# sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"
#
# sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
# # => "name='foo''bar' and group_id='4'"
# # => "name='foo''bar' and group_id=4"
#
# sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
# # => "name='foo''bar' and group_id='4'"
Expand All @@ -29,8 +29,8 @@ def sanitize_sql_for_conditions(condition)
end
alias :sanitize_sql :sanitize_sql_for_conditions

# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
# Accepts an array, hash, or string of SQL conditions and sanitizes them into
# a valid SQL fragment for a SET clause, using the adapter of the current `connection`.
#
# sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
# # => "name=NULL and group_id=4"
Expand All @@ -39,7 +39,7 @@ def sanitize_sql_for_conditions(condition)
# # => "name=NULL and group_id=4"
#
# Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
# # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
# # => "\"name\" = NULL, \"group_id\" = 4"
#
# sanitize_sql_for_assignment("name=NULL and group_id='4'")
# # => "name=NULL and group_id='4'"
Expand All @@ -51,10 +51,10 @@ def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
end
end

# Accepts an array, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for an ORDER clause.
# Accepts an array, or string of SQL conditions and sanitizes them into
# a valid SQL fragment for an ORDER clause, using the adapter of the current `connection`.
#
# sanitize_sql_for_order([Arel.sql("field(id, ?)"), [1,3,2]])
# sanitize_sql_for_order([Arel.sql("field(id, ?)"), [1, 3, 2]])
# # => "field(id, 1,3,2)"
#
# sanitize_sql_for_order("id ASC")
Expand All @@ -78,10 +78,11 @@ def sanitize_sql_for_order(condition)
end
end

# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET
# clause, using the adapter of the current `connection`.
#
# sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
# # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
# # => "\"status\" = NULL, \"group_id\" = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
Expand Down Expand Up @@ -114,8 +115,8 @@ def sanitize_sql_like(string, escape_character = "\\")
string.gsub(/(?=[%_])/, escape_character)
end

# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
# Accepts an array of conditions. The array has each value sanitized and interpolated into
# the SQL statement, valid for the adapter of the current `connection`
#
# sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
# # => "name='foo''bar' and group_id=4"
Expand Down
91 changes: 91 additions & 0 deletions activerecord/test/cases/adapters/mysql2/sanitize_test.rb
@@ -0,0 +1,91 @@
# frozen_string_literal: true

require "cases/helper"
require "models/post"

class Mysql2SanitizeTest < ActiveRecord::Mysql2TestCase
def test_sanitize_sql_for_conditions_from_docs
actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_assignment_from_docs
actual = Post.sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
expected = "name=NULL and group_id='4'"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
expected = "name=NULL and group_id='4'"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
expected = "`posts`.`name` = NULL, `posts`.`group_id` = 4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment("name=NULL and group_id='4'")
expected = "name=NULL and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_order_from_docs
skip "See https://github.com/rails/rails/issues/44312"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently broken #44312 and is what I am working to fix next.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to fix it all in the same PR, I'd be worried the skip may be forgotten otherwise.

actual = Post.sanitize_sql_for_order([Arel.sql("field(id, ?)"), [1, 3, 2]])
expected = "field(id, 1,3,2)"
assert_equal expected, actual

actual = Post.sanitize_sql_for_order("id ASC")
expected = "id ASC"
assert_equal expected, actual
end

def test_sanitize_sql_hash_for_assignment_from_docs
actual = Post.sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
expected = "`posts`.`status` = NULL, `posts`.`group_id` = 1"
assert_equal expected, actual
end

def test_sanitize_sql_like_from_docs
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the like tests do not depend on the connection adapter. But they easily could in future. I always opt for better and proactive test coverage, so added the same tests in all three adapters.

actual = ActiveRecord::Base.sanitize_sql_like("100%")
expected = "100\\%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string")
expected = "snake\\_cased\\_string"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("100%", "!")
expected = "100!%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string", "!")
expected = "snake!_cased!_string"
assert_equal expected, actual
end

def test_sanitize_sql_array_from_docs
actual = ActiveRecord::Base.sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo\\'bar' and group_id='4'"
assert_equal expected, actual
end
end
90 changes: 90 additions & 0 deletions activerecord/test/cases/adapters/postgresql/sanitize_test.rb
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require "cases/helper"
require "models/post"

class PostgreSQLSanitizeTest < ActiveRecord::PostgreSQLTestCase
def test_sanitize_sql_for_conditions_from_docs
actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_assignment_from_docs
actual = Post.sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
expected = "name=NULL and group_id=4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
expected = "name=NULL and group_id=4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
expected = "\"name\" = NULL, \"group_id\" = 4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment("name=NULL and group_id='4'")
expected = "name=NULL and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_order_from_docs
actual = Post.sanitize_sql_for_order([Arel.sql("field(id, ?)"), [1, 3, 2]])
expected = "field(id, 1,3,2)"
assert_equal expected, actual

actual = Post.sanitize_sql_for_order("id ASC")
expected = "id ASC"
assert_equal expected, actual
end

def test_sanitize_sql_hash_for_assignment_from_docs
actual = Post.sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
expected = "\"status\" = NULL, \"group_id\" = 1"
assert_equal expected, actual
end

def test_sanitize_sql_like_from_docs
actual = ActiveRecord::Base.sanitize_sql_like("100%")
expected = "100\\%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string")
expected = "snake\\_cased\\_string"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("100%", "!")
expected = "100!%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string", "!")
expected = "snake!_cased!_string"
assert_equal expected, actual
end

def test_sanitize_sql_array_from_docs
actual = ActiveRecord::Base.sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual
end
end
90 changes: 90 additions & 0 deletions activerecord/test/cases/adapters/sqlite3/sanitize_test.rb
@@ -0,0 +1,90 @@
# frozen_string_literal: true

require "cases/helper"
require "models/post"

class SQLite3SanitizeTest < ActiveRecord::SQLite3TestCase
def test_sanitize_sql_for_conditions_from_docs
actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_assignment_from_docs
actual = Post.sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
expected = "name=NULL and group_id=4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
expected = "name=NULL and group_id=4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
expected = "\"name\" = NULL, \"group_id\" = 4"
assert_equal expected, actual

actual = Post.sanitize_sql_for_assignment("name=NULL and group_id='4'")
expected = "name=NULL and group_id='4'"
assert_equal expected, actual
end

def test_sanitize_sql_for_order_from_docs
actual = Post.sanitize_sql_for_order([Arel.sql("field(id, ?)"), [1, 3, 2]])
expected = "field(id, 1,3,2)"
assert_equal expected, actual

actual = Post.sanitize_sql_for_order("id ASC")
expected = "id ASC"
assert_equal expected, actual
end

def test_sanitize_sql_hash_for_assignment_from_docs
actual = Post.sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
expected = "\"status\" = NULL, \"group_id\" = 1"
assert_equal expected, actual
end

def test_sanitize_sql_like_from_docs
actual = ActiveRecord::Base.sanitize_sql_like("100%")
expected = "100\\%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string")
expected = "snake\\_cased\\_string"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("100%", "!")
expected = "100!%"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_like("snake_cased_string", "!")
expected = "snake!_cased!_string"
assert_equal expected, actual
end

def test_sanitize_sql_array_from_docs
actual = ActiveRecord::Base.sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
expected = "name='foo''bar' and group_id=4"
assert_equal expected, actual

actual = ActiveRecord::Base.sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
expected = "name='foo''bar' and group_id='4'"
assert_equal expected, actual
end
end