diff --git a/Gemfile.lock b/Gemfile.lock index de5f2bc92fa00..e37d749f22768 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -26,63 +26,63 @@ GIT PATH remote: . specs: - actioncable (5.2.7) - actionpack (= 5.2.7) + actioncable (5.2.7.1) + actionpack (= 5.2.7.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.7) - actionpack (= 5.2.7) - actionview (= 5.2.7) - activejob (= 5.2.7) + actionmailer (5.2.7.1) + actionpack (= 5.2.7.1) + actionview (= 5.2.7.1) + activejob (= 5.2.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.7) - actionview (= 5.2.7) - activesupport (= 5.2.7) + actionpack (5.2.7.1) + actionview (= 5.2.7.1) + activesupport (= 5.2.7.1) rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.7) - activesupport (= 5.2.7) + actionview (5.2.7.1) + activesupport (= 5.2.7.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.7) - activesupport (= 5.2.7) + activejob (5.2.7.1) + activesupport (= 5.2.7.1) globalid (>= 0.3.6) - activemodel (5.2.7) - activesupport (= 5.2.7) - activerecord (5.2.7) - activemodel (= 5.2.7) - activesupport (= 5.2.7) + activemodel (5.2.7.1) + activesupport (= 5.2.7.1) + activerecord (5.2.7.1) + activemodel (= 5.2.7.1) + activesupport (= 5.2.7.1) arel (>= 9.0) - activestorage (5.2.7) - actionpack (= 5.2.7) - activerecord (= 5.2.7) + activestorage (5.2.7.1) + actionpack (= 5.2.7.1) + activerecord (= 5.2.7.1) marcel (~> 1.0.0) - activesupport (5.2.7) + activesupport (5.2.7.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - rails (5.2.7) - actioncable (= 5.2.7) - actionmailer (= 5.2.7) - actionpack (= 5.2.7) - actionview (= 5.2.7) - activejob (= 5.2.7) - activemodel (= 5.2.7) - activerecord (= 5.2.7) - activestorage (= 5.2.7) - activesupport (= 5.2.7) + rails (5.2.7.1) + actioncable (= 5.2.7.1) + actionmailer (= 5.2.7.1) + actionpack (= 5.2.7.1) + actionview (= 5.2.7.1) + activejob (= 5.2.7.1) + activemodel (= 5.2.7.1) + activerecord (= 5.2.7.1) + activestorage (= 5.2.7.1) + activesupport (= 5.2.7.1) bundler (>= 1.3.0) - railties (= 5.2.7) + railties (= 5.2.7.1) sprockets-rails (>= 2.0.0) - railties (5.2.7) - actionpack (= 5.2.7) - activesupport (= 5.2.7) + railties (5.2.7.1) + actionpack (= 5.2.7.1) + activesupport (= 5.2.7.1) method_source rake (>= 0.8.7) thor (>= 0.19.0, < 2.0) @@ -274,7 +274,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.14.0) + loofah (2.16.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) diff --git a/RAILS_VERSION b/RAILS_VERSION index 32a40681516ab..120e232312c5c 100644 --- a/RAILS_VERSION +++ b/RAILS_VERSION @@ -1 +1 @@ -5.2.7 +5.2.7.1 diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index cd3565bb83be1..932fa7d83192c 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index a06b023a21d0f..6d9aeafe7afe4 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actioncable/package.json b/actioncable/package.json index 9e9baf30fac35..2c34f59d69973 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -1,6 +1,6 @@ { "name": "actioncable", - "version": "5.2.7", + "version": "5.2.7-1", "description": "WebSocket framework for Ruby on Rails.", "main": "lib/assets/compiled/action_cable.js", "files": [ diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 21b06632f88a3..bdf7a884bb4be 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb index 233a6c25e03da..1caf9f180787b 100644 --- a/actionmailer/lib/action_mailer/gem_version.rb +++ b/actionmailer/lib/action_mailer/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index d67bd2645ac0c..1b94e862e39f6 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,8 +1,13 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* Allow Content Security Policy DSL to generate for API responses. + + *Tim Wade* + ## Rails 5.2.7 (March 10, 2022) ## * No changes. - ## Rails 5.2.6.3 (March 08, 2022) ## * No changes. diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index 6f9fb11a2950b..69ac64c5a3142 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -17,7 +17,6 @@ def call(env) request = ActionDispatch::Request.new env _, headers, _ = response = @app.call(env) - return response unless html_response?(headers) return response if policy_present?(headers) if policy = request.content_security_policy @@ -30,13 +29,6 @@ def call(env) end private - - def html_response?(headers) - if content_type = headers[CONTENT_TYPE] - content_type =~ /html/ - end - end - def header_name(request) if request.content_security_policy_report_only POLICY_REPORT_ONLY diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 606c9149e70f0..98f4d702b1f36 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index 71ac63def2184..25feb4c258826 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -343,6 +343,11 @@ class PolicyController < ActionController::Base content_security_policy_report_only only: :report_only + content_security_policy only: :api do |p| + p.default_src :none + p.frame_ancestors :none + end + def index head :ok end @@ -367,6 +372,10 @@ def no_policy head :ok end + def api + render json: {} + end + private def condition? params[:condition] == "true" @@ -382,6 +391,7 @@ def condition? get "/report-only", to: "policy#report_only" get "/script-src", to: "policy#script_src" get "/no-policy", to: "policy#no_policy" + get "/api", to: "policy#api" end end @@ -448,6 +458,11 @@ def test_generates_no_content_security_policy assert_nil response.headers["Content-Security-Policy-Report-Only"] end + def test_generates_api_security_policy + get "/api" + assert_policy "default-src 'none'; frame-ancestors 'none'" + end + private def assert_policy(expected, report_only: false) diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 9d93af32fb90b..aa5e0cfebdd7b 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,15 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* Fix and add protections for XSS in `ActionView::Helpers` and `ERB::Util`. + + Escape dangerous characters in names of tags and names of attributes in the + tag helpers, following the XML specification. Rename the option + `:escape_attributes` to `:escape`, to simplify by applying the option to the + whole tag. + + *Álvaro Martín Fraguas* + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index 3533f9410b792..d6a7ebb1e41cd 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a6cec3f69cfaa..9f5d90fc854a3 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -41,18 +41,25 @@ def initialize(view_context) @view_context = view_context end - def tag_string(name, content = nil, escape_attributes: true, **options, &block) + def tag_string(name, content = nil, **options, &block) + escape = handle_deprecated_escape_options(options) content = @view_context.capture(self, &block) if block_given? + if VOID_ELEMENTS.include?(name) && content.nil? - "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe + "<#{name.to_s.dasherize}#{tag_options(options, escape)}>".html_safe else - content_tag_string(name.to_s.dasherize, content || "", options, escape_attributes) + content_tag_string(name.to_s.dasherize, content || "", options, escape) end end def content_tag_string(name, content, options, escape = true) tag_options = tag_options(options, escape) if options - content = ERB::Util.unwrapped_html_escape(content) if escape + + if escape + name = ERB::Util.xml_name_escape(name) + content = ERB::Util.unwrapped_html_escape(content) + end + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}".html_safe end @@ -85,6 +92,8 @@ def boolean_tag_option(key) end def tag_option(key, value, escape) + key = ERB::Util.xml_name_escape(key) if escape + if value.is_a?(Array) value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) else @@ -106,8 +115,29 @@ def respond_to_missing?(*args) true end - def method_missing(called, *args, &block) - tag_string(called, *args, &block) + def handle_deprecated_escape_options(options) + # The option :escape_attributes has been merged into the options hash to be + # able to warn when it is used, so we need to handle default values here. + escape_option_provided = options.has_key?(:escape) + escape_attributes_option_provided = options.has_key?(:escape_attributes) + + if escape_attributes_option_provided + ActiveSupport::Deprecation.warn(<<~MSG) + Use of the option :escape_attributes is deprecated. It currently \ + escapes both names and values of tags and attributes and it is \ + equivalent to :escape. If any of them are enabled, the escaping \ + is fully enabled. + MSG + end + + return true unless escape_option_provided || escape_attributes_option_provided + escape_option = options.delete(:escape) + escape_attributes_option = options.delete(:escape_attributes) + escape_option || escape_attributes_option + end + + def method_missing(called, *args, **options, &block) + tag_string(called, *args, **options, &block) end end @@ -236,6 +266,7 @@ def tag(name = nil, options = nil, open = false, escape = true) if name.nil? tag_builder else + name = ERB::Util.xml_name_escape(name) if escape "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe end end diff --git a/actionview/package.json b/actionview/package.json index 798fb04926b60..af01b0a73cabf 100644 --- a/actionview/package.json +++ b/actionview/package.json @@ -1,6 +1,6 @@ { "name": "rails-ujs", - "version": "5.2.7", + "version": "5.2.7-1", "description": "Ruby on Rails unobtrusive scripting adapter", "main": "lib/assets/compiled/rails-ujs.js", "files": [ diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index 9a6226fd04f1a..2f53826c9cfa7 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -7,6 +7,8 @@ class TagHelperTest < ActionView::TestCase tests ActionView::Helpers::TagHelper + COMMON_DANGEROUS_CHARS = "&<>\"' %*+,/;=^|" + def test_tag assert_equal "
", tag("br") assert_equal "
", tag(:br, clear: "left") @@ -79,6 +81,84 @@ def test_tag_builder_options_converts_boolean_option tag.p(disabled: true, itemscope: true, multiple: true, readonly: true, allowfullscreen: true, seamless: true, typemustmatch: true, sortable: true, default: true, inert: true, truespeed: true) end + def test_tag_builder_do_not_modify_html_safe_options + html_safe_str = '"'.html_safe + assert_equal "

", tag("p", value: html_safe_str) + assert_equal '"', html_safe_str + assert html_safe_str.html_safe? + end + + def test_tag_with_dangerous_name + assert_equal "<#{"_" * COMMON_DANGEROUS_CHARS.size} />", + tag(COMMON_DANGEROUS_CHARS) + + assert_equal "<#{COMMON_DANGEROUS_CHARS} />", + tag(COMMON_DANGEROUS_CHARS, nil, false, false) + end + + def test_tag_builder_with_dangerous_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "<#{escaped_dangerous_chars}>", + tag.public_send(COMMON_DANGEROUS_CHARS.to_sym) + + assert_equal "<#{COMMON_DANGEROUS_CHARS}>", + tag.public_send(COMMON_DANGEROUS_CHARS.to_sym, nil, escape: false) + end + + def test_tag_with_dangerous_aria_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag("the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "", + tag("the-name", { aria: { COMMON_DANGEROUS_CHARS => "the value" } }, false, false) + end + + def test_tag_builder_with_dangerous_aria_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag.public_send(:"the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "", + tag.public_send(:"the-name", aria: { COMMON_DANGEROUS_CHARS => "the value" }, escape: false) + end + + def test_tag_with_dangerous_data_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag("the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "", + tag("the-name", { data: { COMMON_DANGEROUS_CHARS => "the value" } }, false, false) + end + + def test_tag_builder_with_dangerous_data_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag.public_send(:"the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }) + + assert_equal "", + tag.public_send(:"the-name", data: { COMMON_DANGEROUS_CHARS => "the value" }, escape: false) + end + + def test_tag_with_dangerous_unknown_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag("the-name", COMMON_DANGEROUS_CHARS => "the value") + + assert_equal "", + tag("the-name", { COMMON_DANGEROUS_CHARS => "the value" }, false, false) + end + + def test_tag_builder_with_dangerous_unknown_attribute_name + escaped_dangerous_chars = "_" * COMMON_DANGEROUS_CHARS.size + assert_equal "", + tag.public_send(:"the-name", COMMON_DANGEROUS_CHARS => "the value") + + assert_equal "", + tag.public_send(:"the-name", COMMON_DANGEROUS_CHARS => "the value", escape: false) + end + def test_content_tag assert_equal "Create", content_tag("a", "Create", "href" => "create") assert_predicate content_tag("a", "Create", "href" => "create"), :html_safe? @@ -98,7 +178,7 @@ def test_tag_builder_with_content assert_equal "

<script>evil_js</script>

", tag.p("") assert_equal "

", - tag.p("", escape_attributes: false) + tag.p("", escape: false) end def test_tag_builder_nested @@ -213,10 +293,10 @@ def test_content_tag_with_unescaped_array_class end def test_tag_builder_with_unescaped_array_class - str = tag.p "limelight", class: ["song", "play>"], escape_attributes: false + str = tag.p "limelight", class: ["song", "play>"], escape: false assert_equal "

\">limelight

", str - str = tag.p "limelight", class: ["song", ["play>"]], escape_attributes: false + str = tag.p "limelight", class: ["song", ["play>"]], escape: false assert_equal "

\">limelight

", str end @@ -235,7 +315,7 @@ def test_content_tag_with_unescaped_empty_array_class end def test_tag_builder_with_unescaped_empty_array_class - str = tag.p "limelight", class: [], escape_attributes: false + str = tag.p "limelight", class: [], escape: false assert_equal '

limelight

', str end @@ -306,11 +386,11 @@ def test_disable_escaping end def test_tag_builder_disable_escaping - assert_equal '', tag.a(href: "&", escape_attributes: false) - assert_equal 'cnt', tag.a(href: "&", escape_attributes: false) { "cnt" } - assert_equal '
', tag.br("data-hidden": "&", escape_attributes: false) - assert_equal 'content', tag.a("content", href: "&", escape_attributes: false) - assert_equal 'content', tag.a(href: "&", escape_attributes: false) { "content" } + assert_equal '', tag.a(href: "&", escape: false) + assert_equal 'cnt', tag.a(href: "&", escape: false) { "cnt" } + assert_equal '
', tag.br("data-hidden": "&", escape: false) + assert_equal 'content', tag.a("content", href: "&", escape: false) + assert_equal 'content', tag.a(href: "&", escape: false) { "content" } end def test_data_attributes diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 2930fc428b16c..cbe4d9e8c62bc 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb index 08b1e3ffd07f2..5f8ab664b29d2 100644 --- a/activejob/lib/active_job/gem_version.rb +++ b/activejob/lib/active_job/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index cb1952ef02ade..105781aa9edd0 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb index 888e3835a2f7e..d131b73d55219 100644 --- a/activemodel/lib/active_model/gem_version.rb +++ b/activemodel/lib/active_model/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 29e9c575cd881..3420c0fb8b9a8 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 96223419be93f..5cbdb243abe99 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 11358061b938b..0a9b0d40267ed 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * Fix `ActiveStorage.supported_image_processing_methods` and diff --git a/activestorage/lib/active_storage/gem_version.rb b/activestorage/lib/active_storage/gem_version.rb index 2584dd79e07c1..00b0c9a526098 100644 --- a/activestorage/lib/active_storage/gem_version.rb +++ b/activestorage/lib/active_storage/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activestorage/package.json b/activestorage/package.json index 35081d95c322e..87a52b96d5518 100644 --- a/activestorage/package.json +++ b/activestorage/package.json @@ -1,6 +1,6 @@ { "name": "activestorage", - "version": "5.2.7", + "version": "5.2.7-1", "description": "Attach cloud and local files in Rails applications", "main": "app/assets/javascripts/activestorage.js", "files": [ diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 754a49d96f865..fd008179848c8 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,13 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* Fix and add protections for XSS in `ActionView::Helpers` and `ERB::Util`. + + Add the method `ERB::Util.xml_name_escape` to escape dangerous characters + in names of tags and names of attributes, following the specification of XML. + + *Álvaro Martín Fraguas* + + ## Rails 5.2.7 (March 10, 2022) ## * Restore support to Ruby 2.2. diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index f3bdc2977e6e6..7a9fecf5e923e 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -12,6 +12,14 @@ module Util HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u + # Following XML requirements: https://www.w3.org/TR/REC-xml/#NT-Name + TAG_NAME_START_REGEXP_SET = ":A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}\u{37F}-\u{1FFF}" \ + "\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}" \ + "\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}" + TAG_NAME_START_REGEXP = /[^#{TAG_NAME_START_REGEXP_SET}]/ + TAG_NAME_FOLLOWING_REGEXP = /[^#{TAG_NAME_START_REGEXP_SET}\-.0-9\u{B7}\u{0300}-\u{036F}\u{203F}-\u{2040}]/ + TAG_NAME_REPLACEMENT_CHAR = "_" + # A utility method for escaping HTML tag characters. # This method is also aliased as h. # @@ -116,6 +124,26 @@ def json_escape(s) end module_function :json_escape + + # A utility method for escaping XML names of tags and names of attributes. + # + # xml_name_escape('1 < 2 & 3') + # # => "1___2___3" + # + # It follows the requirements of the specification: https://www.w3.org/TR/REC-xml/#NT-Name + def xml_name_escape(name) + name = name.to_s + return "" if name.blank? + + starting_char = name[0].gsub(TAG_NAME_START_REGEXP, TAG_NAME_REPLACEMENT_CHAR) + + return starting_char if name.size == 1 + + following_chars = name[1..-1].gsub(TAG_NAME_FOLLOWING_REGEXP, TAG_NAME_REPLACEMENT_CHAR) + + starting_char + following_chars + end + module_function :xml_name_escape end end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 9e75e10edc9c3..cd2dfe6dff82b 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 472190277e267..2dcd7234c002f 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -866,6 +866,32 @@ def to_s expected = "© <" assert_equal expected, ERB::Util.html_escape_once(string) end + + test "ERB::Util.xml_name_escape should escape unsafe characters for XML names" do + unsafe_char = ">" + safe_char = "Á" + safe_char_after_start = "3" + + assert_equal "_", ERB::Util.xml_name_escape(unsafe_char) + assert_equal "_#{safe_char}", ERB::Util.xml_name_escape(unsafe_char + safe_char) + assert_equal "__", ERB::Util.xml_name_escape(unsafe_char * 2) + + assert_equal "__#{safe_char}_", + ERB::Util.xml_name_escape("#{unsafe_char * 2}#{safe_char}#{unsafe_char}") + + assert_equal safe_char + safe_char_after_start, + ERB::Util.xml_name_escape(safe_char + safe_char_after_start) + + assert_equal "_#{safe_char}", + ERB::Util.xml_name_escape(safe_char_after_start + safe_char) + + assert_equal "img_src_nonexistent_onerror_alert_1_", + ERB::Util.xml_name_escape("img src=nonexistent onerror=alert(1)") + + common_dangerous_chars = "&<>\"' %*+,/;=^|" + assert_equal "_" * common_dangerous_chars.size, + ERB::Util.xml_name_escape(common_dangerous_chars) + end end class StringExcludeTest < ActiveSupport::TestCase diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 4b9c2ce5f192d..068c49f7ee5ea 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index e01fa13cf0763..cf3a93a6d57a2 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,8 @@ +## Rails 5.2.7.1 (April 26, 2022) ## + +* No changes. + + ## Rails 5.2.7 (March 10, 2022) ## * No changes. diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb index 38a2e8c5c771c..470606919d01e 100644 --- a/railties/lib/rails/gem_version.rb +++ b/railties/lib/rails/gem_version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/version.rb b/version.rb index 38a2e8c5c771c..470606919d01e 100644 --- a/version.rb +++ b/version.rb @@ -10,7 +10,7 @@ module VERSION MAJOR = 5 MINOR = 2 TINY = 7 - PRE = nil + PRE = "1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end