CVE-2017-2662
Description
A flaw was found in Foreman's katello plugin version 3.4.5. After setting a new role to allow restricted access on a repository with a filter (filter set on the Product Name), the filter is not respected when the actions are done via hammer using the repository id.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A flaw in Foreman's katello plugin 3.4.5 fails to enforce repository filters when actions are performed via hammer using the repository ID.
Vulnerability
A flaw was found in Foreman's Katello plugin version 3.4.5 [1]. When a role is configured to restrict access to a repository with a filter set on the Product Name, the filter is not respected if actions are performed via the hammer CLI using the repository ID instead of the product name [1]. This bypass allows users with restricted roles to interact with repositories they should not have access to.
Exploitation
An attacker with a restricted role that includes a filter on Product Name can bypass the intended access control by using the hammer CLI and specifying the repository ID directly [1]. No additional authentication or privileges are required beyond having a restricted role that normally would limit access via product name filters. The hammer commands that use repository IDs will succeed even when the user lacks the necessary permissions for that repository.
Impact
Successful exploitation allows an attacker to perform actions (such as viewing or managing content) on repositories that should be restricted based on the filter [1]. This leads to unauthorized access to sensitive content, potentially resulting in information disclosure or further compromise of the system.
Mitigation
The issue has been addressed in the Katello codebase via commit [2] and pull request [4]. Users should update to a version of the Katello plugin that includes the fix. No workaround is mentioned in the references, but restricting hammer CLI access or using product names instead of repository IDs may reduce risk. If no patch is applied, the vulnerability remains exploitable.
AI Insight generated on May 22, 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 |
|---|---|---|
katelloRubyGems | < 3.17.0.rc1 | 3.17.0.rc1 |
Affected products
2- The Foreman Project/foreman katello pluginv5Range: 3.4.5
Patches
1853260e3e9f9Fixes #18035 - filter repos for CV with perms
5 files changed · +193 −1
app/controllers/katello/api/v2/api_controller.rb+1 −0 modified@@ -5,6 +5,7 @@ class Api::V2::ApiController < ::Api::V2::BaseController include Api::V2::Rendering include Api::V2::ErrorHandling include ::Foreman::Controller::CsvResponder + include Concerns::Api::V2::AssociationsPermissionCheck # support for session (thread-local) variables must be the last filter in this class include Foreman::ThreadSession::Cleaner
app/controllers/katello/api/v2/content_views_controller.rb+7 −0 modified@@ -22,6 +22,13 @@ class Api::V2::ContentViewsController < Api::V2::ApiController param :solve_dependencies, :bool, :desc => N_("Solve RPM dependencies by default on Content View publish, defaults to false") end + def filtered_associations + { + :component_ids => Katello::ContentViewVersion, + :repository_ids => Katello::Repository + } + end + api :GET, "/organizations/:organization_id/content_views", N_("List content views") api :GET, "/content_views", N_("List content views") param :organization_id, :number, :desc => N_("organization identifier")
app/controllers/katello/concerns/api/v2/associations_permission_check.rb+67 −0 added@@ -0,0 +1,67 @@ +module Katello + module Concerns + module Api::V2::AssociationsPermissionCheck + extend ActiveSupport::Concern + + # The purpose of this module is to protect a controller from a user creating or updating some association + # when they do not have permissions to view the associated items. An example would be adding random repository ids to + # content view. + # To support this, within the controller define a method such as: + # def filtered_associations + # { + # :component_ids => Katello::ContentViewVersion, + # :repository_ids => Katello::Repository + # } + # end + # This assumes that the parameters are 'wrapped'. So the above in the content_views_controller, actually looks at + # a subhash of 'content_view' + + included do + before_action :check_association_ids, :only => [:create, :update] + end + + def check_association_ids + if filtered_associations + wrapped_params = params[self._wrapper_options.name] + find_param_arrays(wrapped_params).each do |key_path| + if (model_class = filtered_associations.with_indifferent_access.dig(*key_path)) + param_ids = wrapped_params.dig(*key_path) + filtered_ids = model_class.readable.where(:id => param_ids).pluck(:id) + if (unfound_ids = param_ids_missing(param_ids, filtered_ids)).any? + fail HttpErrors::NotFound, _("One or more ids (%{ids}) were not found for %{assoc}. You may not have permissions to see them.") % + {ids: unfound_ids, assoc: key_path.last} + end + else + fail _("Unfiltered params array: %s.") % key_path + end + end + else + Rails.logger.warn("#{self.class.name} may has unprotected associations, see associations_permission_check.rb for details.") if ENV['RAILS_ENV'] == 'development' + end + end + + def filtered_associations + #should return {} when supported by all controllers + nil + end + + def param_ids_missing(param_ids, filtered_ids) + param_ids.map(&:to_i).uniq - filtered_ids.map(&:to_i).uniq + end + + #returns an array of list of keys pointing to an array in a params hash i.e.: + # {"a"=> {"b" => [3]}} => [["a", "b"]] + def find_param_arrays(hash = params) + list_of_paths = [] + hash.each do |key, value| + if value.is_a?(ActionController::Parameters) || value.is_a?(Hash) + list_of_paths += find_param_arrays(value).compact.map { |inner_keys| [key] + inner_keys } + elsif value.is_a?(Array) + list_of_paths << [key] + end + end + list_of_paths.compact + end + end + end +end
test/controllers/api/v2/concerns/associations_permission_check_test.rb+78 −0 added@@ -0,0 +1,78 @@ +# encoding: utf-8 + +require "katello_test_helper" + +module Katello + class TestAssociationIdController + def initialize(params, filtered_associations) + @params = params + @filtered_associations = filtered_associations + end + + def self.before_action(*_args) + end + + include Concerns::Api::V2::AssociationsPermissionCheck + + attr_accessor :filtered_associations + attr_reader :params + + def _wrapper_options + OpenStruct.new(:name => :content_view) + end + end + + class Api::V2::AssociationsPermissionCheckTest < ActiveSupport::TestCase + def setup + @cv = katello_content_views(:acme_default) + @repo = katello_repositories(:fedora_17_x86_64) + + @params = { + content_view: { + foo: [@cv.id], + foo2: 3, + foo3: { + baz: [@repo.id], + baz2: 9 + } + } + } + + @filtered_associations = { + foo: ::Katello::ContentView, + foo3: { + baz: ::Katello::Repository + } + } + end + + def test_find_param_arrays + controller = TestAssociationIdController.new(@params, @filtered_associations) + assert_equal [[:content_view, :foo], [:content_view, :foo3, :baz]].sort, controller.find_param_arrays.sort + end + + def test_check_association_ids_positive + controller = TestAssociationIdController.new(@params, @filtered_associations) + + controller.check_association_ids + end + + def test_check_association_ids_not_found_id + @params[:content_view][:foo] << -1 + controller = TestAssociationIdController.new(@params, @filtered_associations) + + assert_raises(Katello::HttpErrors::NotFound) do + controller.check_association_ids + end + end + + def test_check_association_ids_not_defined + @params[:content_view][:not_defined] = [1] + controller = TestAssociationIdController.new(@params, @filtered_associations) + + assert_raises(StandardError) do + controller.check_association_ids + end + end + end +end
test/controllers/api/v2/content_views_controller_test.rb+40 −1 modified@@ -125,6 +125,30 @@ def test_create_protected end end + def test_create_protected_without_repo_read + user = User.unscoped.find(users(:restricted).id) + denied_perms = [@create_permission, @view_permission, @update_permission, :destroy_content_views] + setup_user_with_permissions(denied_perms, user) + repository = katello_repositories(:fedora_17_unpublished) + login_user(user) + + post :create, params: {:organization_id => @organization.id, content_view: { :name => "Test", :repository_ids => [repository.id] } } + + assert_response 404 + end + + def test_create_protected_without_repo_read_non_wrapped + user = User.unscoped.find(users(:restricted).id) + denied_perms = [@create_permission, @view_permission, @update_permission, :destroy_content_views] + setup_user_with_permissions(denied_perms, user) + repository = katello_repositories(:fedora_17_unpublished) + + login_user(user) + post :create, params: { :name => "Test", :organization_id => @organization.id, :repository_ids => [repository.id] } + + assert_response 404 + end + def test_create_with_non_json_request @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' post :create, params: { :name => "My View", :description => "Cool", :organization_id => @organization.id } @@ -175,6 +199,21 @@ def test_update def test_update_repositories repository = katello_repositories(:fedora_17_unpublished) + params = { :repository_ids => [repository.id] } + assert_sync_task(::Actions::Katello::ContentView::Update) do |_content_view, content_view_params| + content_view_params.key?(:repository_ids).must_equal true + content_view_params[:repository_ids].must_equal params[:repository_ids] + end + put :update, params: { :id => @library_dev_staging_view.id, :content_view => params } + + assert_response :success + assert_template layout: 'katello/api/v2/layouts/resource' + assert_template 'katello/api/v2/common/update' + end + + def test_update_repositories_strings + repository = katello_repositories(:fedora_17_unpublished) + params = { :repository_ids => [repository.id.to_s] } assert_sync_task(::Actions::Katello::ContentView::Update) do |_content_view, content_view_params| content_view_params.key?(:repository_ids).must_equal true @@ -191,7 +230,7 @@ def test_update_components version = @library_dev_staging_view.versions.first composite = ContentView.find(katello_content_views(:composite_view).id) - params = { :component_ids => [version.id.to_s] } + params = { :component_ids => [version.id] } assert_sync_task(::Actions::Katello::ContentView::Update) do |_content_view, content_view_params| content_view_params.key?(:component_ids).must_equal true content_view_params[:component_ids].must_equal params[:component_ids]
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-cpv6-pfq6-j2v7ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2017-2662ghsaADVISORY
- bugzilla.redhat.com/show_bug.cgighsax_refsource_CONFIRMWEB
- github.com/Katello/katello/commit/853260e3e9f94179d5881199e7885d1c08e600f6ghsaWEB
- github.com/Katello/katello/pull/8772ghsaWEB
- projects.theforeman.org/issues/18838ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.