Cross-site Scripting in view_component
Description
VIewComponent is a framework for building view components in Ruby on Rails. Versions prior to 2.31.2 and 2.49.1 contain a cross-site scripting vulnerability that has the potential to impact anyone using translations with the view_component gem. Data received via user input and passed as an interpolation argument to the translate method is not properly sanitized before display. Versions 2.31.2 and 2.49.1 have been released and fully mitigate the vulnerability. As a workaround, avoid passing user input to the translate function, or sanitize the inputs before passing them.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Cross-site scripting (XSS) vulnerability in ViewComponent's `translate` method allows injection of malicious HTML via user-supplied interpolation arguments.
Vulnerability
ViewComponent versions prior to 2.31.2 and 2.49.1 contain a cross-site scripting (XSS) vulnerability in the translate helper method. When user-supplied data is passed as an interpolation argument to translate, it is not properly sanitized before being rendered in the view. This affects any application using translations with the view_component gem. [1]
Exploitation
An attacker can inject arbitrary HTML or JavaScript by providing malicious input that is used as an interpolation argument in a translate call. No special network position is required beyond the ability to submit data that reaches the vulnerable component. The attacker does not need authentication if the component is publicly accessible. The user interaction is limited to the attacker submitting the payload; the victim views the page containing the translated output. [1]
Impact
Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's browser session, leading to potential data theft, session hijacking, or defacement. The impact is limited to the client side, but can affect any user who views the compromised component. [1]
Mitigation
The vulnerability is fully mitigated in versions 2.31.2 and 2.49.1, released on 2022-03-02. Users should upgrade to these versions or later. As a workaround, avoid passing user input to the translate function, or sanitize inputs before passing them. [1][2]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
view_componentRubyGems | >= 2.31.0, < 2.31.2 | 2.31.2 |
view_componentRubyGems | >= 2.32.0, < 2.49.1 | 2.49.1 |
Affected products
2- github/view_componentv5Range: >= 2.31.0, < 2.31.2
Patches
13f82a6e62578Fix XSS vulnerability when using HTML-safe translations and interpolation arguments (#1295)
5 files changed · +116 −69
docs/CHANGELOG.md+12 −0 modified@@ -53,6 +53,12 @@ title: Changelog *Bernie Chiu* +## 2.49.1 + +* Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments. + + *Cameron Dutro* + ## 2.49.0 * Fix path handling for evaluated view components that broke in Ruby 3.1. @@ -659,6 +665,12 @@ title: Changelog *Joel Hawksley* +## 2.31.2 + +* Patch XSS vulnerability in `ViewComponent::Translatable` module caused by improperly escaped interpolation arguments. + + *Cameron Dutro* + ## 2.31.1 * Fix `DEPRECATION WARNING: before_render_check` when compiling `ViewComponent::Base`
Gemfile.lock+66 −69 modified@@ -1,74 +1,74 @@ PATH remote: . specs: - view_component (2.48.0) + view_component (2.49.0) activesupport (>= 5.0.0, < 8.0) method_source (~> 1.0) GEM remote: https://rubygems.org/ specs: - actioncable (7.0.1) - actionpack (= 7.0.1) - activesupport (= 7.0.1) + actioncable (7.0.2) + actionpack (= 7.0.2) + activesupport (= 7.0.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.1) - actionpack (= 7.0.1) - activejob (= 7.0.1) - activerecord (= 7.0.1) - activestorage (= 7.0.1) - activesupport (= 7.0.1) + actionmailbox (7.0.2) + actionpack (= 7.0.2) + activejob (= 7.0.2) + activerecord (= 7.0.2) + activestorage (= 7.0.2) + activesupport (= 7.0.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.1) - actionpack (= 7.0.1) - actionview (= 7.0.1) - activejob (= 7.0.1) - activesupport (= 7.0.1) + actionmailer (7.0.2) + actionpack (= 7.0.2) + actionview (= 7.0.2) + activejob (= 7.0.2) + activesupport (= 7.0.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.1) - actionview (= 7.0.1) - activesupport (= 7.0.1) + actionpack (7.0.2) + actionview (= 7.0.2) + activesupport (= 7.0.2) rack (~> 2.0, >= 2.2.0) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.1) - actionpack (= 7.0.1) - activerecord (= 7.0.1) - activestorage (= 7.0.1) - activesupport (= 7.0.1) + actiontext (7.0.2) + actionpack (= 7.0.2) + activerecord (= 7.0.2) + activestorage (= 7.0.2) + activesupport (= 7.0.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.1) - activesupport (= 7.0.1) + actionview (7.0.2) + activesupport (= 7.0.2) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (7.0.1) - activesupport (= 7.0.1) + activejob (7.0.2) + activesupport (= 7.0.2) globalid (>= 0.3.6) - activemodel (7.0.1) - activesupport (= 7.0.1) - activerecord (7.0.1) - activemodel (= 7.0.1) - activesupport (= 7.0.1) - activestorage (7.0.1) - actionpack (= 7.0.1) - activejob (= 7.0.1) - activerecord (= 7.0.1) - activesupport (= 7.0.1) + activemodel (7.0.2) + activesupport (= 7.0.2) + activerecord (7.0.2) + activemodel (= 7.0.2) + activesupport (= 7.0.2) + activestorage (7.0.2) + actionpack (= 7.0.2) + activejob (= 7.0.2) + activerecord (= 7.0.2) + activesupport (= 7.0.2) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.1) + activesupport (7.0.2) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -120,13 +120,13 @@ GEM temple (>= 0.8.0) tilt html_tokenizer (0.0.7) - i18n (1.8.11) + i18n (1.10.0) concurrent-ruby (~> 1.0) io-wait (0.2.1) jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) - loofah (2.13.0) + loofah (2.14.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -135,7 +135,7 @@ GEM matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.2) - mini_portile2 (2.7.1) + mini_portile2 (2.8.0) minitest (5.6.0) net-imap (0.2.3) digest @@ -153,11 +153,11 @@ GEM net-protocol timeout nio4r (2.5.8) - nokogiri (1.13.1) - mini_portile2 (~> 2.7.0) + nokogiri (1.13.3) + mini_portile2 (~> 2.8.0) racc (~> 1.4) parallel (1.21.0) - parser (3.1.0.0) + parser (3.1.1.0) ast (~> 2.4.1) pry (0.14.1) coderay (~> 1.1) @@ -167,35 +167,35 @@ GEM rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) - rails (7.0.1) - actioncable (= 7.0.1) - actionmailbox (= 7.0.1) - actionmailer (= 7.0.1) - actionpack (= 7.0.1) - actiontext (= 7.0.1) - actionview (= 7.0.1) - activejob (= 7.0.1) - activemodel (= 7.0.1) - activerecord (= 7.0.1) - activestorage (= 7.0.1) - activesupport (= 7.0.1) + rails (7.0.2) + actioncable (= 7.0.2) + actionmailbox (= 7.0.2) + actionmailer (= 7.0.2) + actionpack (= 7.0.2) + actiontext (= 7.0.2) + actionview (= 7.0.2) + activejob (= 7.0.2) + activemodel (= 7.0.2) + activerecord (= 7.0.2) + activestorage (= 7.0.2) + activesupport (= 7.0.2) bundler (>= 1.15.0) - railties (= 7.0.1) + railties (= 7.0.2) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (7.0.1) - actionpack (= 7.0.1) - activesupport (= 7.0.1) + railties (7.0.2) + actionpack (= 7.0.2) + activesupport (= 7.0.2) method_source rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) rake (13.0.6) - regexp_parser (2.2.0) + regexp_parser (2.2.1) rexml (3.2.5) rubocop (1.13.0) parallel (~> 1.10) @@ -206,8 +206,8 @@ GEM rubocop-ast (>= 1.2.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.15.1) - parser (>= 3.0.1.1) + rubocop-ast (1.16.0) + parser (>= 3.1.1.0) rubocop-github (0.16.2) rubocop (<= 1.13.0) rubocop-performance (<= 1.11.0) @@ -232,7 +232,7 @@ GEM temple (>= 0.7.6, < 0.9) tilt (>= 2.0.6, < 2.1) smart_properties (1.17.0) - sprockets (4.0.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.2) @@ -259,7 +259,7 @@ GEM webrick (~> 1.7.0) yard-activesupport-concern (0.0.1) yard (>= 0.8) - zeitwerk (2.5.3) + zeitwerk (2.5.4) PLATFORMS ruby @@ -274,11 +274,8 @@ DEPENDENCIES haml (~> 5) jbuilder (~> 2) minitest (= 5.6.0) - net-imap - net-pop - net-smtp pry (~> 0.13) - rails (~> 7.0.0) + rails (= 7.0.2) rake (~> 13.0) rubocop-github (~> 0.16.1) simplecov (~> 0.18.0)
lib/view_component/translatable.rb+19 −0 modified@@ -1,5 +1,6 @@ # frozen_string_literal: true +require "erb" require "set" require "i18n" require "action_view/helpers/translation_helper" @@ -70,6 +71,10 @@ def translate(key = nil, **options) key = key&.to_s unless key.is_a?(String) key = "#{i18n_scope}#{key}" if key.start_with?(".") + if HTML_SAFE_TRANSLATION_KEY.match?(key) + html_escape_translation_options!(options) + end + if key.start_with?(i18n_scope + ".") translated = catch(:exception) do @@ -107,5 +112,19 @@ def html_safe_translation(translation) translation.html_safe # rubocop:disable Rails/OutputSafety end end + + private + + def html_escape_translation_options!(options) + options.each do |name, value| + unless i18n_option?(name) || (name == :count && value.is_a?(Numeric)) + options[name] = ERB::Util.html_escape(value.to_s) + end + end + end + + def i18n_option?(name) + (@i18n_option_names ||= I18n::RESERVED_KEYS.to_set).include?(name) + end end end
test/sandbox/app/components/translatable_component.yml+2 −0 modified@@ -3,6 +3,8 @@ en: hello_html: "Hello from <strong>sidecar translations</strong>!" + interpolated_html: "There are %{horse_count} horses in the <strong>barn</strong>!" + html: "hello <em>world</em>!" from:
test/view_component/translatable_test.rb+17 −0 modified@@ -58,6 +58,23 @@ def test_translate_marks_translations_with_a_html_suffix_as_safe_html assert_predicate translate(".hello_html"), :html_safe? end + def test_translate_with_html_suffix_escapes_interpolated_arguments + translation = translate(".interpolated_html", horse_count: "<script type='text/javascript'>alert('foo');</script>") + assert_equal( + "There are <script type='text/javascript'>alert('foo');</script> horses in the "\ + "<strong>barn</strong>!", + translation + ) + end + + def test_translate_with_html_suffix_does_not_double_escape + translation = translate(".interpolated_html", horse_count: "> 4") + assert_equal( + "There are > 4 horses in the <strong>barn</strong>!", + translation + ) + end + def test_translate_uses_the_helper_when_no_sidecar_file_is_provided # The cache needs to be kept clean for TranslatableComponent, otherwise it will rely on the # already created i18n_backend.
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-cm9w-c4rj-r2cfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-24722ghsaADVISORY
- github.com/github/view_component/commit/3f82a6e62578ff6f361aba24a1feb2caccf83ff9ghsax_refsource_MISCWEB
- github.com/github/view_component/releases/tag/v2.31.2ghsax_refsource_MISCWEB
- github.com/github/view_component/releases/tag/v2.49.1ghsax_refsource_MISCWEB
- github.com/github/view_component/security/advisories/GHSA-cm9w-c4rj-r2cfghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/view_component/CVE-2022-24722.ymlghsaWEB
News mentions
0No linked articles in our index yet.