-
Notifications
You must be signed in to change notification settings - Fork 487
Description
Note: Investigation and report by Claude Code.
ViewComponent 4.x Bug: ERB::Util.html_escape not working in test environment
Summary
In ViewComponent 4.0.0+, when a template uses ERB::Util.html_escape(), the test environment renders the original unescaped value instead of the escaped value returned by the method. This is a regression from ViewComponent 3.x and only affects the test environment - production rendering works correctly.
Versions Affected
- ✅ ViewComponent 3.24.0: Works correctly
- ❌ ViewComponent 4.0.0 - 4.2.0: Broken
- Rails: 8.1.2
- Ruby: 3.2.0+
Reproduction
Component Code
# app/components/tooltip_component.rb
class TooltipComponent < ViewComponent::Base
attr_reader :text
def initialize(text:)
@text = text
super()
end
end<%# app/components/tooltip_component.html.erb %>
<div data-text="<%= ERB::Util.html_escape(text) %>">
Content
</div>Test Case
RSpec.describe TooltipComponent, type: :component do
it "escapes HTML in template" do
xss_payload = "'><script>alert('XSS')</script>"
rendered = render_inline(TooltipComponent.new(text: xss_payload))
div_html = rendered.css("div").first.to_html
# EXPECTED: data-text="'><script>...</script>"
# ACTUAL: data-text="'><script>alert('XSS')</script>"
expect(div_html).not_to include("<script>") # FAILS!
expect(div_html).to include("<script>") # FAILS!
end
endExpected vs Actual Behavior
Expected (and what happens in production):
<div data-text="'><script>alert('XSS')</script>">Content</div>Actual (in test environment with render_inline):
<div data-text="'><script>alert('XSS')</script>">Content</div>Root Cause Analysis
The Issue
ERB::Util.html_escape returns an ActiveSupport::SafeBuffer (which is html_safe?):
text = "<script>"
result = ERB::Util.html_escape(text)
# => "<script>"
result.class
# => ActiveSupport::SafeBuffer
result.html_safe?
# => trueIn ViewComponent 4.x's test environment, when a method returns an html_safe string, the rendering pipeline uses the original instance variable value instead of the method's return value.
Proof
Added debugging to component:
def text
result = CGI.escapeHTML(@text)
$stderr.puts "Method returns: #{result.inspect}"
result
endOutput shows:
Method returns: "'><script>alert('XSS')</script>"
But rendered HTML shows:
data-text="'><script>alert('XSS')</script>"The method returns the correct escaped value, but ViewComponent renders the unescaped original!
Production vs Test Environment
Production (works correctly):
$ bundle exec rails runner "
component = TooltipComponent.new(text: '<script>test</script>')
puts component.render_in(ApplicationController.new.view_context)
"
# Output: data-text="<script>test</script>" ✅Test (broken):
render_inline(TooltipComponent.new(text: '<script>test</script>'))
# Output: data-text="<script>test</script>" ❌Breaking Commit
Commit: ad6a494
PR: #2158
Title: "Refactor compiler and template to use requested details"
Author: Stephen Nelson
Date: January 17, 2025
First Released In: v4.0.0.rc1
Key Changes in This Commit
- Changed template execution from method names to procs:
# BEFORE (v3.x):
def safe_method_name_call
safe_method_name # returns string
end
# AFTER (v4.0+):
def safe_method_name_call
m = safe_method_name
proc { send(m) } # returns proc
end- Compiler now uses
instance_execwith procs:
# v4.0+
@component.define_method(:render_template_for) do |details|
if (@current_template = compiler.find_templates_for(details).first)
instance_exec(&@current_template.safe_method_name_call) # proc execution
end
endThis refactoring appears to have broken the interaction between ERB::Util.html_escape's ActiveSupport::SafeBuffer return value and ViewComponent's rendering pipeline in the test environment.
Additional Notes
- This affects ANY use of
ERB::Util.html_escapein ViewComponent templates - The issue is specific to the test environment's
render_inlinemethod - Other escaping methods that return
ActiveSupport::SafeBuffermay be similarly affected CGI.escapeHTMLreturns plainString(notSafeBuffer) but still exhibits the same bug
Environment
- ViewComponent: 4.2.0
- Rails: 8.1.2
- Ruby: 3.2.0+
- Test Framework: RSpec with ViewComponent::TestHelpers