Moderate severityNVD Advisory· Published Mar 20, 2026· Updated Mar 24, 2026
Avo has a XSS vulnerability on `return_to` param
CVE-2026-33209
Description
Avo is a framework to create admin panels for Ruby on Rails apps. Prior to version 3.30.3, a reflected cross-site scripting (XSS) vulnerability exists in the return_to query parameter used in the avo interface. An attacker can craft a malicious URL that injects arbitrary JavaScript, which is executed when he clicks a dynamically generated navigation button. This issue has been patched in version 3.30.3.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
avoRubyGems | < 3.30.3 | 3.30.3 |
Affected products
1Patches
14453d39ddc63fix: `return_to` parameter (#4330)
7 files changed · +75 −12
app/components/avo/referrer_params_component.html.erb+1 −1 modified@@ -4,5 +4,5 @@ <%= hidden_field_tag :via_record_id, params[:via_record_id] if params[:via_record_id] %> <%= hidden_field_tag :via_relation, params[:via_relation] if params[:via_relation] %> <%= hidden_field_tag :via_belongs_to_resource_class, params[:via_belongs_to_resource_class] if params[:via_belongs_to_resource_class] %> -<%= hidden_field_tag :return_to, params[:return_to] if params[:return_to].present? %> +<%= hidden_field_tag :return_to, e(params[:return_to]) if params[:return_to].present? %> <%= hidden_field_tag :referrer, @back_path if params[:via_resource_class] %>
app/components/avo/views/resource_show_component.rb+1 −1 modified@@ -57,7 +57,7 @@ def edit_path # When coming from a turbo frame, we don't want to return to that exact frame # for example when editing a has_one field we want to return to the parent frame # not the frame of the has_one field. - args[:return_to] = request.url unless request.url.include?("turbo_frame=") + args[:return_to] = e(request.fullpath) unless request.url.include?("turbo_frame=") helpers.edit_resource_path(record: @resource.record, resource: @resource, **args) end
app/controllers/avo/base_application_controller.rb+0 −1 modified@@ -326,7 +326,6 @@ def raise_404 def decode_params if params[:return_to].present? - params[:raw_return_to] = params[:return_to] params[:return_to] = d(params[:return_to]) end end
app/controllers/avo/base_controller.rb+3 −1 modified@@ -546,7 +546,9 @@ def redirect_path_from_resource_option(action = :after_update_path) return nil if @resource.class.send(action).blank? extra_args = {} - extra_args[:return_to] = params[:return_to] if params[:return_to].present? + if params[:return_to].present? + extra_args[:return_to] = e(params[:return_to]) + end if @resource.class.send(action) == :index resources_path(resource: @resource, **extra_args)
app/helpers/avo/application_helper.rb+5 −3 modified@@ -161,14 +161,16 @@ def container_classes # encode & encrypt params def e(value) - Avo::Services::EncryptionService.encrypt(message: value, purpose: :return_to, serializer: Marshal) + encrypted = Avo::Services::EncryptionService.encrypt(message: value, purpose: :return_to, serializer: Marshal) + Base64.urlsafe_encode64(encrypted, padding: false) end # decrypt & decode params def d(value) - Avo::Services::EncryptionService.decrypt(message: value, purpose: :return_to, serializer: Marshal) + decoded = Base64.urlsafe_decode64(value.to_s) + Avo::Services::EncryptionService.decrypt(message: decoded, purpose: :return_to, serializer: Marshal) rescue - value + nil end private
spec/system/avo/app_spec.rb+37 −0 modified@@ -1,4 +1,5 @@ require "rails_helper" +require "cgi" RSpec.describe "App", type: :system do let!(:user) { create :user } @@ -83,5 +84,41 @@ visit '/admin/resources/projects?per_page=1&turbo_frame=has_many_field_show_test_xgc2pf><script>alert("XSS")<%2Fscript>p9sk5' expect { accept_alert }.to raise_error(Capybara::ModalNotFound) end + + it "sanitizes unsafe return_to values" do + project = projects.first + payload = CGI.escape("javascript:alert('Reflected XSS')") + + visit "/admin/resources/projects/#{project.id}?return_to=#{payload}" + + go_back_link = find("div[data-target='panel-tools'] a", text: "Go back") + expect(go_back_link[:href]).to end_with("/admin/resources/projects") + expect(go_back_link[:href]).not_to start_with("javascript:") + + expect { + accept_alert do + click_on "Go back" + end + }.to raise_error(Capybara::ModalNotFound) + + expect(page).to have_current_path "/admin/resources/projects" + end + + it "keeps valid internal return_to paths" do + project = projects.first + encryption_helper = Class.new do + include Avo::ApplicationHelper + end.new + return_to = "/admin/resources/projects/new" + + visit "/admin/resources/projects/#{project.id}?return_to=#{encryption_helper.e(return_to)}" + + go_back_link = find("div[data-target='panel-tools'] a", text: "Go back") + expect(go_back_link[:href]).to end_with(return_to) + + click_on "Go back" + + expect(page).to have_current_path return_to + end end end
spec/system/avo/open_field_attachment_spec.rb+28 −5 modified@@ -1,7 +1,9 @@ require "rails_helper" +require "timeout" RSpec.describe "OpenFieldAttachment", type: :system do include ActionView::RecordIdentifier + let!(:user) { create :user } context "with PDF attachment" do @@ -30,12 +32,33 @@ expect(link[:target]).to eq("_blank") expect(link[:rel]).to eq("noopener noreferrer") - new_window = window_opened_by { link.click } - expect(new_window).not_to be_nil + new_windows = [] + + begin + existing_windows = page.windows + link.click + + Timeout.timeout(Capybara.default_max_wait_time) do + loop do + new_windows = page.windows - existing_windows + break if new_windows.any? + + sleep 0.05 + end + end - within_window new_window do - # Check that final URL ends with filename, since path changes after redirect - expect(page.current_url).to match(/dummy-file\.pdf\z/) + expect(new_windows).not_to be_empty + expect(new_windows.any? do |window| + within_window(window) do + page.has_current_path?(/dummy-file\.pdf\z/, url: true, wait: 2) || page.current_url.include?(file_path) + end + end).to be(true) + ensure + new_windows.each do |window| + window.close if page.windows.include?(window) + rescue Ferrum::BrowserError, Capybara::WindowError + # CI can auto-dispose PDF popup targets before explicit close. + end end end end
Vulnerability mechanics
Generated by null/stub 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-762r-27w2-q22jghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-33209ghsaADVISORY
- github.com/avo-hq/avo/commit/4453d39ddc6309f3bc8ada73ef21e1971112de7dghsax_refsource_MISCWEB
- github.com/avo-hq/avo/pull/4330ghsax_refsource_MISCWEB
- github.com/avo-hq/avo/releases/tag/v3.30.3ghsax_refsource_MISCWEB
- github.com/avo-hq/avo/security/advisories/GHSA-762r-27w2-q22jghsax_refsource_CONFIRMWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/avo/CVE-2026-33209.ymlghsaWEB
News mentions
0No linked articles in our index yet.