VYPR
High severityNVD Advisory· Published Feb 9, 2023· Updated Aug 2, 2024

CVE-2023-22794

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.

PackageAffected versionsPatched versions
activerecordRubyGems
>= 6.0.0, < 6.0.6.16.0.6.1
activerecordRubyGems
>= 6.1.0, < 6.1.7.16.1.7.1
activerecordRubyGems
>= 7.0.0, < 7.0.4.17.0.4.1

Affected products

3

Patches

1
d7aba06953f9

Make sanitize_as_sql_comment more strict

https://github.com/rails/railsJohn HawthornSep 6, 2022via ghsa
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

News mentions

0

No linked articles in our index yet.