CVE-2024-21636 (Ruby, published 2024-01-04T20:15Z)
Description: value: view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. Versions prior to 3.9.0 and 2.83.0 have a cross-site scripting vulnerability that has the potential to impact anyone rendering a component directly from a controller with the view_component gem. Note that only components that define a `#call` method (i.e. instead of using a sidecar template) are affected. The return value of the `#call` method is not sanitized and can include user-defined content. In addition, the return value of the `#output_postamble` methodis not sanitized, which can also lead to cross-site scripting issues. Versions 3.9.0 and 2.83.0 have been released and fully mitigate both the `#call` and the `#output_postamble` vulnerabilities. As a workaround, sanitize the return value of `#call`.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/ViewComponent/view_component
Files changed:
.github/workflows/ci.yml
.rubocop.yml
docs/CHANGELOG.md
lib/view_component/base.rb
lib/view_component/compiler.rb
lib/view_component/test_helpers.rb
test/sandbox/app/components/after_render_component.rb
test/sandbox/app/components/content_predicate_component.rb
test/sandbox/app/components/custom_test_controller_component.rb
test/sandbox/app/components/inherited_from_uncompilable_component.rb
test/sandbox/app/components/inline_render_component.rb
test/sandbox/app/components/message_component.rb
test/sandbox/app/components/render_check_component.rb
test/sandbox/app/components/unsafe_component.rb
test/sandbox/app/components/unsafe_postamble_component.rb
test/sandbox/app/components/variant_ivar_component.rb
test/sandbox/app/controllers/integration_examples_controller.rb
test/sandbox/config/routes.rb
test/sandbox/test/collection_test.rb
test/sandbox/test/integration_test.rb
test/sandbox/test/rendering_test.rb
test/test_helper.rb
Commit message: Ensure HTML output safety (#1950)



Vulnerable code:
        name: simplecov-resultset-rails${{matrix.rails_version}}-ruby${{matrix.ruby_version}}
          render_template_for(@__vc_variant).to_s
          render_template_for(@__vc_variant).to_s + output_postamble
      ""
    def with_request_url(full_path, host: nil, method: nil)
    "Hello, "
    "World!"
      "Default"
    helpers.foo
    "<div>hello world</div>"
    @items.map { |c| render(c) }.join
    @message
    "Rendered"
    @variant.to_s
        "<div data-name='#{product.name}'><h1>#{product.name}</h1></div>"
    render_inline(JbuilderComponent.new(message: "bar")) { "foo" }
    render_inline(ContentPredicateComponent.new.with_content("foo"))

Fixed code:
        name: simplecov-resultset-rails${{matrix.rails_version}}-ruby${{matrix.ruby_version}}-${{matrix.mode}}
ruby_version: 2.5
* Ensure HTML output safety.

    *Cameron Dutro*

          safe_render_template_for(@__vc_variant).to_s
          safe_render_template_for(@__vc_variant).to_s + safe_output_postamble
      @@default_output_postamble ||= "".html_safe
    def maybe_escape_html(text)
      return text if request && !request.format.html?
      return text if text.nil? || text.empty?

      if text.html_safe?
        text
      else
        yield
        html_escape(text)
      end
    end

    def safe_render_template_for(variant)
      if compiler.renders_template_for_variant?(variant)
        render_template_for(variant)
      else
        maybe_escape_html(render_template_for(variant)) do
          Kernel.warn("WARNING: The #{self.class} component rendered HTML-unsafe output. The output will be automatically escaped, but you may want to investigate.")
        end
      end
    end

    def safe_output_postamble
      maybe_escape_html(output_postamble) do
        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.")
      end
    end

    def compiler
      @compiler ||= self.class.compiler
    end

      @variants_rendering_templates = Set.new
          @variants_rendering_templates << template[:variant]
    def renders_template_for_variant?(variant)
      @variants_rendering_templates.include?(variant)
    end

    def with_request_url(full_path, host: nil, method: nil, format: :html)
      old_request_format = vc_test_request.format.symbol
      vc_test_request.format = format
      vc_test_request.format = old_request_format
    "Hello, ".html_safe
    "World!".html_safe
      "Default".html_safe
    html_escape(helpers.foo)
    "<div>hello world</div>".html_safe
    @items.map { |c| render(c) }.join.html_safe
    html_escape(@message)
    "Rendered".html_safe
# frozen_string_literal: true

class UnsafeComponent < ViewComponent::Base
  def call
    user_input = "<script>alert('hello!')</script>"

    "<div>hello #{user_input}</div>"
  end
end
# frozen_string_literal: true

class UnsafePostambleComponent < ViewComponent::Base
  def call
    "<div>some content</div>".html_safe
  end

  def output_postamble
    "<script>alert('hello!')</script>"
  end
end
    html_escape(@variant.to_s)

  def unsafe_component
    render(UnsafeComponent.new)
  end

  def unsafe_postamble_component
    render(UnsafePostambleComponent.new)
  end
  get :unsafe_component, to: "integration_examples#unsafe_component"
  get :unsafe_postamble_component, to: "integration_examples#unsafe_postamble_component"
        "<div data-name='#{product.name}'><h1>#{product.name}</h1></div>".html_safe

  def test_unsafe_component
    warnings = capture_warnings { get "/unsafe_component" }
    assert_select("script", false)
    assert(
      warnings.any? { |warning| warning.include?("component rendered HTML-unsafe output") },
      "Rendering UnsafeComponent did not emit an HTML safety warning"
    )
  end

  def test_unsafe_postamble_component
    warnings = capture_warnings { get "/unsafe_postamble_component" }
    assert_select("script", false)
    assert(
      warnings.any? { |warning| warning.include?("component was provided an HTML-unsafe postamble") },
      "Rendering UnsafePostambleComponent did not emit an HTML safety warning"
    )
  end
    with_request_url("/", format: :json) do
      render_inline(JbuilderComponent.new(message: "bar")) { "foo" }
    end
    render_inline(ContentPredicateComponent.new.with_content("foo".html_safe))

def capture_warnings(&block)
  [].tap do |warnings|
    Kernel.stub(:warn, ->(msg) { warnings << msg }) do
      block.call
    end
  end
end