CVE-2023-22794
Description
A vulnerability in ActiveRecord <6.0.6.1, v6.1.7.1 and v7.0.4.1 related to the sanitization of comments. If malicious user input is passed to either the annotate query method, the optimizer_hints query method, or through the QueryLogs interface which automatically adds annotations, it may be sent to the database withinsufficient sanitization and be able to inject SQL outside of the comment.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
SQL injection in ActiveRecord <6.0.6.1, 6.1.7.1, 7.0.4.1 due to insufficient comment sanitization, allowing attacker input to break out of SQL comments.
Background
ActiveRecord is the Object-Relational Mapping (ORM) layer of Ruby on Rails, responsible for translating Ruby method calls into SQL queries. Certain ActiveRecord methods, such as annotate and optimizer_hints, as well as the QueryLogs interface, allow developers to attach SQL comments to generated queries. A vulnerability in versions prior to 6.0.6.1, 6.1.7.1, and 7.0.4.1 arises from insufficient sanitization of user-supplied strings passed to these comment mechanisms [1][2][4].
Exploitation
If an attacker can control the string passed to annotate, optimizer_hints, or a dynamic QueryLogs tag, they can inject SQL outside of the intended comment boundary. For example, Post.where(id: 1).annotate("#{params[:user_input]}") could allow the input to terminate the comment and append arbitrary SQL [4]. The attacker does not need database-level privileges but does require a way to inject into one of these Rails methods—typically through unsanitized HTTP parameters or other user-controlled data [1][4].
Impact
Successful exploitation enables SQL injection, which can lead to unauthorized data access, data modification, or even full database compromise. Because ActiveRecord provides high-level access to the underlying database, an attacker could read, write, or delete records without proper authorization [1][4].
Mitigation
Patches are available in ActiveRecord versions 6.0.6.1, 6.1.7.1, and 7.0.4.1. Users are strongly advised to upgrade immediately. As a workaround, developers should avoid passing untrusted user input to annotate, optimizer_hints, or any QueryLogs tag that dynamically includes external data [2][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
activerecordRubyGems | >= 6.0.0, < 6.0.6.1 | 6.0.6.1 |
activerecordRubyGems | >= 6.1.0, < 6.1.7.1 | 6.1.7.1 |
activerecordRubyGems | >= 7.0.0, < 7.0.4.1 | 7.0.4.1 |
Affected products
3- ActiveRecord/ActiveRecorddescription
- ghsa-coords2 versions
>= 6.0.0, < 6.0.6.1+ 1 more
- (no CPE)range: >= 6.0.0, < 6.0.6.1
- (no CPE)range: < 7.0.4.1-1.1
Patches
1d7aba06953f9Make sanitize_as_sql_comment more strict
6 files changed · +38 −14
activerecord/lib/active_record/connection_adapters/abstract/quoting.rb+10 −1 modified@@ -146,7 +146,16 @@ def quoted_binary(value) # :nodoc: end def sanitize_as_sql_comment(value) # :nodoc: - value.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "") + # Sanitize a string to appear within a SQL comment + # For compatibility, this also surrounding "/*+", "/*", and "*/" + # charcacters, possibly with single surrounding space. + # Then follows that by replacing any internal "*/" or "/ *" with + # "* /" or "/ *" + comment = value.to_s.dup + comment.gsub!(%r{\A\s*/\*\+?\s?|\s?\*/\s*\Z}, "") + comment.gsub!("*/", "* /") + comment.gsub!("/*", "/ *") + comment end def column_name_matcher # :nodoc:
activerecord/lib/active_record/query_logs.rb+12 −1 modified@@ -33,6 +33,8 @@ module ActiveRecord # want to add to the comment. Dynamic content can be created by setting a proc or lambda value in a hash, # and can reference any value stored in the +context+ object. # + # Escaping is performed on the string returned, however untrusted user input should not be used. + # # Example: # # tags = [ @@ -109,7 +111,16 @@ def uncached_comment end def escape_sql_comment(content) - content.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "") + # Sanitize a string to appear within a SQL comment + # For compatibility, this also surrounding "/*+", "/*", and "*/" + # charcacters, possibly with single surrounding space. + # Then follows that by replacing any internal "*/" or "/ *" with + # "* /" or "/ *" + comment = content.to_s.dup + comment.gsub!(%r{\A\s*/\*\+?\s?|\s?\*/\s*\Z}, "") + comment.gsub!("*/", "* /") + comment.gsub!("/*", "/ *") + comment end def tag_content
activerecord/lib/active_record/relation/query_methods.rb+2 −0 modified@@ -1216,6 +1216,8 @@ def skip_preloading! # :nodoc: # # SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */ # # The SQL block comment delimiters, "/*" and "*/", will be added automatically. + # + # Some escaping is performed, however untrusted user input should not be used. def annotate(*args) check_if_method_has_arguments!(__callee__, args) spawn.annotate!(*args)
activerecord/test/cases/annotate_test.rb+8 −3 modified@@ -18,17 +18,22 @@ def test_annotate_wraps_content_in_an_inline_comment def test_annotate_is_sanitized quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts") - assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* /foo/ \* \*/}i) do posts = Post.select(:id).annotate("*/foo/*") assert posts.first end - assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do + assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \*\* //foo// \*\* \*/}i) do posts = Post.select(:id).annotate("**//foo//**") assert posts.first end - assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do + assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* \* //foo// \* \* \*/}i) do + posts = Post.select(:id).annotate("* *//foo//* *") + assert posts.first + end + + assert_sql(%r{SELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \* /foo/ \* \*/ /\* \* /bar \*/}i) do posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") assert posts.first end
activerecord/test/cases/query_logs_test.rb+3 −2 modified@@ -42,8 +42,9 @@ def test_escaping_good_comment end def test_escaping_bad_comments - assert_equal "; DROP TABLE USERS;", ActiveRecord::QueryLogs.send(:escape_sql_comment, "*/; DROP TABLE USERS;/*") - assert_equal "; DROP TABLE USERS;", ActiveRecord::QueryLogs.send(:escape_sql_comment, "**//; DROP TABLE USERS;/*") + assert_equal "* /; DROP TABLE USERS;/ *", ActiveRecord::QueryLogs.send(:escape_sql_comment, "*/; DROP TABLE USERS;/*") + assert_equal "** //; DROP TABLE USERS;/ *", ActiveRecord::QueryLogs.send(:escape_sql_comment, "**//; DROP TABLE USERS;/*") + assert_equal "* * //; DROP TABLE USERS;// * *", ActiveRecord::QueryLogs.send(:escape_sql_comment, "* *//; DROP TABLE USERS;//* *") end def test_basic_commenting
activerecord/test/cases/relation_test.rb+3 −7 modified@@ -345,7 +345,7 @@ def test_relation_with_annotation_chains_sql_comments def test_relation_with_annotation_filters_sql_comment_delimiters post_with_annotation = Post.where(id: 1).annotate("**//foo//**") - assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql + assert_includes post_with_annotation.to_sql, "= 1 /* ** //foo// ** */" end def test_relation_with_annotation_includes_comment_in_count_query @@ -367,13 +367,9 @@ def test_relation_without_annotation_does_not_include_an_empty_comment def test_relation_with_optimizer_hints_filters_sql_comment_delimiters post_with_hint = Post.where(id: 1).optimizer_hints("**//BADHINT//**") - assert_match %r{BADHINT}, post_with_hint.to_sql - assert_no_match %r{\*/BADHINT}, post_with_hint.to_sql - assert_no_match %r{\*//BADHINT}, post_with_hint.to_sql - assert_no_match %r{BADHINT/\*}, post_with_hint.to_sql - assert_no_match %r{BADHINT//\*}, post_with_hint.to_sql + assert_includes post_with_hint.to_sql, "/*+ ** //BADHINT// ** */" post_with_hint = Post.where(id: 1).optimizer_hints("/*+ BADHINT */") - assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql + assert_includes post_with_hint.to_sql, "/*+ BADHINT */" end def test_does_not_duplicate_optimizer_hints_on_merge
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-hq7p-j377-6v63ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-22794ghsaADVISORY
- www.debian.org/security/2023/dsa-5372ghsavendor-advisoryWEB
- discuss.rubyonrails.org/t/cve-2023-22794-sql-injection-vulnerability-via-activerecord-comments/82117ghsaWEB
- github.com/rails/rails/commit/d7aba06953f9fa789c411676b941d20df8ef73deghsaWEB
- github.com/rails/rails/releases/tag/v7.0.4.1ghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/activerecord/CVE-2023-22794.ymlghsaWEB
- security.netapp.com/advisory/ntap-20240202-0008ghsaWEB
- security.netapp.com/advisory/ntap-20240202-0008/mitre
News mentions
0No linked articles in our index yet.