Skip to content

Commit 0d26944

Browse files
authored
Ensure HTML output safety (#1950)
1 parent 78dbac2 commit 0d26944

22 files changed

+121
-17
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989
uses: actions/[email protected]
9090
if: always()
9191
with:
92-
name: simplecov-resultset-rails${{matrix.rails_version}}-ruby${{matrix.ruby_version}}
92+
name: simplecov-resultset-rails${{matrix.rails_version}}-ruby${{matrix.ruby_version}}-${{matrix.mode}}
9393
path: coverage
9494
primer_view_components_compatibility:
9595
name: Test compatibility with Primer ViewComponents (main)

.rubocop.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
ruby_version: 2.5
12
require:
23
- standard
34
- "rubocop-md"

docs/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ nav_order: 5
3838

3939
*Mitchell Henke*
4040

41+
* Ensure HTML output safety.
42+
43+
*Cameron Dutro*
44+
4145
## 3.8.0
4246

4347
* Use correct value for the `config.action_dispatch.show_exceptions` config option for edge Rails.

lib/view_component/base.rb

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,9 @@ def render_in(view_context, &block)
106106
if render?
107107
# Avoid allocating new string when output_postamble is blank
108108
if output_postamble.blank?
109-
render_template_for(@__vc_variant).to_s
109+
safe_render_template_for(@__vc_variant).to_s
110110
else
111-
render_template_for(@__vc_variant).to_s + output_postamble
111+
safe_render_template_for(@__vc_variant).to_s + safe_output_postamble
112112
end
113113
else
114114
""
@@ -160,7 +160,7 @@ def render_parent_to_string
160160
#
161161
# @return [String]
162162
def output_postamble
163-
""
163+
@@default_output_postamble ||= "".html_safe
164164
end
165165

166166
# Called before rendering the component. Override to perform operations that
@@ -307,6 +307,38 @@ def content_evaluated?
307307
defined?(@__vc_content_evaluated) && @__vc_content_evaluated
308308
end
309309

310+
def maybe_escape_html(text)
311+
return text if request && !request.format.html?
312+
return text if text.nil? || text.empty?
313+
314+
if text.html_safe?
315+
text
316+
else
317+
yield
318+
html_escape(text)
319+
end
320+
end
321+
322+
def safe_render_template_for(variant)
323+
if compiler.renders_template_for_variant?(variant)
324+
render_template_for(variant)
325+
else
326+
maybe_escape_html(render_template_for(variant)) do
327+
Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
328+
end
329+
end
330+
end
331+
332+
def safe_output_postamble
333+
maybe_escape_html(output_postamble) do
334+
Kernel.warn("WARNING: The #{self.class} component was provided an HTML-unsafe postamble. The postamble will be automatically escaped, but you may want to investigate.")
335+
end
336+
end
337+
338+
def compiler
339+
@compiler ||= self.class.compiler
340+
end
341+
310342
# Set the controller used for testing components:
311343
#
312344
# ```ruby

lib/view_component/compiler.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Compiler
1616
def initialize(component_class)
1717
@component_class = component_class
1818
@redefinition_lock = Mutex.new
19+
@variants_rendering_templates = Set.new
1920
end
2021

2122
def compiled?
@@ -68,6 +69,7 @@ def render_template_for(variant = nil)
6869
else
6970
templates.each do |template|
7071
method_name = call_method_name(template[:variant])
72+
@variants_rendering_templates << template[:variant]
7173

7274
redefinition_lock.synchronize do
7375
component_class.silence_redefinition_of_method(method_name)
@@ -89,6 +91,10 @@ def #{method_name}
8991
CompileCache.register(component_class)
9092
end
9193

94+
def renders_template_for_variant?(variant)
95+
@variants_rendering_templates.include?(variant)
96+
end
97+
9298
private
9399

94100
attr_reader :component_class, :redefinition_lock

lib/view_component/test_helpers.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,13 +178,14 @@ def with_controller_class(klass)
178178
# @param full_path [String] The path to set for the current request.
179179
# @param host [String] The host to set for the current request.
180180
# @param method [String] The request method to set for the current request.
181-
def with_request_url(full_path, host: nil, method: nil)
181+
def with_request_url(full_path, host: nil, method: nil, format: :html)
182182
old_request_host = vc_test_request.host
183183
old_request_method = vc_test_request.request_method
184184
old_request_path_info = vc_test_request.path_info
185185
old_request_path_parameters = vc_test_request.path_parameters
186186
old_request_query_parameters = vc_test_request.query_parameters
187187
old_request_query_string = vc_test_request.query_string
188+
old_request_format = vc_test_request.format.symbol
188189
old_controller = defined?(@vc_test_controller) && @vc_test_controller
189190

190191
path, query = full_path.split("?", 2)
@@ -197,6 +198,7 @@ def with_request_url(full_path, host: nil, method: nil)
197198
vc_test_request.set_header("action_dispatch.request.query_parameters",
198199
Rack::Utils.parse_nested_query(query).with_indifferent_access)
199200
vc_test_request.set_header(Rack::QUERY_STRING, query)
201+
vc_test_request.format = format
200202
yield
201203
ensure
202204
vc_test_request.host = old_request_host
@@ -205,6 +207,7 @@ def with_request_url(full_path, host: nil, method: nil)
205207
vc_test_request.path_parameters = old_request_path_parameters
206208
vc_test_request.set_header("action_dispatch.request.query_parameters", old_request_query_parameters)
207209
vc_test_request.set_header(Rack::QUERY_STRING, old_request_query_string)
210+
vc_test_request.format = old_request_format
208211
@vc_test_controller = old_controller
209212
end
210213

test/sandbox/app/components/after_render_component.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
class AfterRenderComponent < ViewComponent::Base
44
def call
5-
"Hello, "
5+
"Hello, ".html_safe
66
end
77

88
def output_postamble
9-
"World!"
9+
"World!".html_safe
1010
end
1111
end

test/sandbox/app/components/content_predicate_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ def call
55
if content?
66
content
77
else
8-
"Default"
8+
"Default".html_safe
99
end
1010
end
1111
end

test/sandbox/app/components/custom_test_controller_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
class CustomTestControllerComponent < ViewComponent::Base
44
def call
5-
helpers.foo
5+
html_escape(helpers.foo)
66
end
77
end

test/sandbox/app/components/inherited_from_uncompilable_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
class InheritedFromUncompilableComponent < UncompilableComponent
44
def call
5-
"<div>hello world</div>"
5+
"<div>hello world</div>".html_safe
66
end
77
end

0 commit comments

Comments
 (0)