Moderate severityNVD Advisory· Published Jun 30, 2011· Updated Apr 29, 2026
CVE-2011-2197
CVE-2011-2197
Description
The cross-site scripting (XSS) prevention feature in Ruby on Rails 2.x before 2.3.12, 3.0.x before 3.0.8, and 3.1.x before 3.1.0.rc2 does not properly handle mutation of safe buffers, which makes it easier for remote attackers to conduct XSS attacks via crafted strings to an application that uses a problematic string method, as demonstrated by the sub method.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
actionpackRubyGems | >= 2.0.0, < 2.3.12 | 2.3.12 |
actionpackRubyGems | >= 3.0.0, < 3.0.8 | 3.0.8 |
activesupportRubyGems | >= 2.0.0, < 2.3.12 | 2.3.12 |
activesupportRubyGems | >= 3.0.0, < 3.0.8 | 3.0.8 |
Affected products
49cpe:2.3:a:rubyonrails:rails:2.0.0:*:*:*:*:*:*:*+ 45 more
- cpe:2.3:a:rubyonrails:rails:2.0.0:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.0.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.0.0:rc2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.0.4:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.1.0:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.1.1:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.1.2:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.2.0:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.2.1:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.2.2:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.10:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.11:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.2:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.3:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.4:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:2.3.9:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:beta:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:beta2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:beta3:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:beta4:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:rc:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.0:rc2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.1:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.1:pre:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.2:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.2:pre:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.3:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.4:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.5:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.5:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.6:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.6:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.6:rc2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.7:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.7:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.7:rc2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.8:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.8:rc2:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.8:rc3:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.0.8:rc4:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.1.0:*:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.1.0:beta1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:rails:3.1.0:rc1:*:*:*:*:*:*
- cpe:2.3:a:rubyonrails:ruby_on_rails:3.0.4:*:*:*:*:*:*:*
- ghsa-coords2 versions
>= 2.0.0, < 2.3.12+ 1 more
- (no CPE)range: >= 2.0.0, < 2.3.12
- (no CPE)range: >= 2.0.0, < 2.3.12
Patches
2ed3796434af6Do not modify a safe buffer in helpers
2 files changed · +35 −31
actionpack/lib/action_view/helpers/text_helper.rb+17 −23 modified@@ -115,13 +115,12 @@ def highlight(text, phrases, *args) end options.reverse_merge!(:highlighter => '<strong class="highlight">\1</strong>') - text = sanitize(text) unless options[:sanitize] == false - if text.blank? || phrases.blank? - text - else + if text.present? && phrases.present? match = Array(phrases).map { |p| Regexp.escape(p) }.join('|') - text.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter]) - end.html_safe + text = text.to_str.gsub(/(#{match})(?!(?:[^<]*?)(?:["'])[^<>]*>)/i, options[:highlighter]) + end + text = sanitize(text) unless options[:sanitize] == false + text end # Extracts an excerpt from +text+ that matches the first instance of +phrase+. @@ -251,14 +250,16 @@ def word_wrap(text, *args) # simple_format("Look ma! A class!", :class => 'description') # # => "<p class='description'>Look ma! A class!</p>" def simple_format(text, html_options={}, options={}) - text = ''.html_safe if text.nil? + text = text ? text.to_str : '' + text = text.dup if text.frozen? start_tag = tag('p', html_options, true) - text = sanitize(text) unless options[:sanitize] == false text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n text.gsub!(/\n\n+/, "</p>\n\n#{start_tag}") # 2+ newline -> paragraph text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br text.insert 0, start_tag - text.html_safe.safe_concat("</p>") + text.concat("</p>") + text = sanitize(text) unless options[:sanitize] == false + text end # Turns all URLs and e-mail addresses into clickable links. The <tt>:link</tt> option @@ -477,7 +478,7 @@ def set_cycle(name, cycle_object) # is yielded and the result is used as the link text. def auto_link_urls(text, html_options = {}, options = {}) link_attributes = html_options.stringify_keys - text.gsub(AUTO_LINK_RE) do + text.to_str.gsub(AUTO_LINK_RE) do scheme, href = $1, $& punctuation = [] @@ -494,33 +495,26 @@ def auto_link_urls(text, html_options = {}, options = {}) end end - link_text = block_given?? yield(href) : href + link_text = block_given? ? yield(href) : href href = 'http://' + href unless scheme - unless options[:sanitize] == false - link_text = sanitize(link_text) - href = sanitize(href) - end - content_tag(:a, link_text, link_attributes.merge('href' => href), !!options[:sanitize]) + punctuation.reverse.join('') + sanitize = options[:sanitize] != false + content_tag(:a, link_text, link_attributes.merge('href' => href), sanitize) + punctuation.reverse.join('') end end end # Turns all email addresses into clickable links. If a block is given, # each email is yielded and the result is used as the link text. def auto_link_email_addresses(text, html_options = {}, options = {}) - text.gsub(AUTO_EMAIL_RE) do + text.to_str.gsub(AUTO_EMAIL_RE) do text = $& if auto_linked?($`, $') text.html_safe else - display_text = (block_given?) ? yield(text) : text - - unless options[:sanitize] == false - text = sanitize(text) - display_text = sanitize(display_text) unless text == display_text - end + display_text = block_given? ? yield(text) : text + display_text = sanitize(display_text) unless options[:sanitize] == false mail_to text, display_text, html_options end end
actionpack/test/template/text_helper_test.rb+18 −8 modified@@ -48,6 +48,10 @@ def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false) end + def test_simple_format_should_not_be_html_safe_when_sanitize_option_is_false + assert !simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false).html_safe? + end + def test_truncate_should_not_be_html_safe assert !truncate("Hello World!", :length => 12).html_safe? end @@ -166,6 +170,13 @@ def test_highlight_with_options_hash ) end + def test_highlight_on_an_html_safe_string + assert_equal( + "<p>This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day</p>", + highlight("<p>This is a beautiful morning, but also a beautiful day</p>".html_safe, "beautiful", :highlighter => '<b>\1</b>') + ) + end + def test_highlight_with_html assert_equal( "<p>This is a <strong class=\"highlight\">beautiful</strong> morning, but also a <strong class=\"highlight\">beautiful</strong> day</p>", @@ -306,13 +317,10 @@ def test_auto_link_parsing end end - def generate_result(link_text, href = nil, escape = false) - href ||= link_text - if escape - %{<a href="#{CGI::escapeHTML href}">#{CGI::escapeHTML link_text}</a>} - else - %{<a href="#{href}">#{link_text}</a>} - end + def generate_result(link_text, href = nil) + href = CGI::escapeHTML(href || link_text) + text = CGI::escapeHTML(link_text) + %{<a href="#{href}">#{text}</a>} end def test_auto_link_should_not_be_html_safe @@ -323,6 +331,8 @@ def test_auto_link_should_not_be_html_safe assert !auto_link('').html_safe?, 'should not be html safe' assert !auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe?, 'should not be html safe' assert !auto_link("hello #{email_raw}").html_safe?, 'should not be html safe' + assert !auto_link(link_raw.html_safe).html_safe?, 'should not be html safe' + assert !auto_link(email_raw.html_safe).html_safe?, 'should not be html safe' end def test_auto_link_email_address @@ -425,7 +435,7 @@ def test_auto_link def test_auto_link_should_sanitize_input_when_sanitize_option_is_not_false link_raw = %{http://www.rubyonrails.com?id=1&num=2} - assert_equal %{<a href="http://www.rubyonrails.com?id=1&num=2">http://www.rubyonrails.com?id=1&num=2</a>}, auto_link(link_raw) + assert_equal %{<a href="http://www.rubyonrails.com?id=1&num=2">http://www.rubyonrails.com?id=1&num=2</a>}, auto_link(link_raw) end def test_auto_link_should_not_sanitize_input_when_sanitize_option_is_false
53a2c0baf2b1Ensure that the strings returned by SafeBuffer#gsub and friends aren't considered html_safe?
2 files changed · +25 −0
activesupport/lib/active_support/core_ext/string/output_safety.rb+13 −0 modified@@ -73,6 +73,7 @@ def html_safe? module ActiveSupport #:nodoc: class SafeBuffer < String + UNSAFE_STRING_METHODS = ["capitalize", "chomp", "chop", "delete", "downcase", "gsub", "lstrip", "next", "reverse", "rstrip", "slice", "squeeze", "strip", "sub", "succ", "swapcase", "tr", "tr_s", "upcase"].freeze alias safe_concat concat def concat(value) @@ -103,6 +104,18 @@ def to_s def to_yaml(*args) to_str.to_yaml(*args) end + + for unsafe_method in UNSAFE_STRING_METHODS + class_eval <<-EOT, __FILE__, __LINE__ + def #{unsafe_method}(*args) + super.to_str + end + + def #{unsafe_method}!(*args) + raise TypeError, "Cannot modify SafeBuffer in place" + end + EOT + end end end
activesupport/test/safe_buffer_test.rb+12 −0 modified@@ -38,4 +38,16 @@ def setup new_buffer = @buffer.to_s assert_equal ActiveSupport::SafeBuffer, new_buffer.class end + + test "Should not return safe buffer from gsub" do + altered_buffer = @buffer.gsub('', 'asdf') + assert_equal 'asdf', altered_buffer + assert !altered_buffer.html_safe? + end + + test "Should not allow gsub! on safe buffers" do + assert_raise TypeError do + @buffer.gsub!('', 'asdf') + end + end end
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
13- groups.google.com/group/rubyonrails-security/msg/663b600d4471e0d4nvdPatchWEB
- openwall.com/lists/oss-security/2011/06/09/2nvdPatchWEB
- openwall.com/lists/oss-security/2011/06/13/9nvdPatchWEB
- weblog.rubyonrails.org/2011/6/8/potential-xss-vulnerability-in-ruby-on-rails-applicationsnvdPatchWEB
- secunia.com/advisories/44789nvdVendor Advisory
- github.com/advisories/GHSA-v9v4-7jp6-8c73ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2011-2197ghsaADVISORY
- lists.fedoraproject.org/pipermail/package-announce/2011-July/062514.htmlnvdWEB
- lists.fedoraproject.org/pipermail/package-announce/2011-June/062090.htmlnvdWEB
- gist.github.com/NZKoz/b2ceb626fc2bcdfe497fghsaWEB
- github.com/rails/rails/commit/53a2c0baf2b128dd4808eca313256f6f4bb8c4cdghsaWEB
- github.com/rails/rails/commit/ed3796434af6069ced6a641293cf88eef3b284daghsaWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2011-2197.ymlghsaWEB
News mentions
0No linked articles in our index yet.