CVE-2013-3703 (published 2018-06-08T17:29Z)

The controller of the Open Build Service API prior to version 2.4.4 is missing a write permission check, allowing an authenticated attacker to add or remove user roles from packages and/or project meta data.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:N/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-275
cwe_name: Permission Issues
description: Weaknesses in this category are related to improper assignment or handling of permissions.

Fix information:
repo_url: https://github.com/openSUSE/open-build-service
commit_details: repo_url: https://github.com/openSUSE/open-build-service
author: Adrian Schröter
author_date: 2013-07-05 17:07:29+02:00
msg: [api] move write permission checks from controller to package and project model

This is just the first part doing the checks. Removing the old ones is another step.
repository: repo_url: https://github.com/openSUSE/open-build-service
repo_name: openSUSE/open-build-service
description: Build and distribute Linux packages from sources in an automatic, consistent and reproducible way #obs
repo_language: Ruby
owner: openSUSE
file_changes: filename: application_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -442,6 +442,14 @@ class ApplicationController < ActionController::API
     render :text => xml_text, :status => http_status
   end
 
+  rescue_from Project::WritePermissionError do |exception|
+    render_error :status => 403, :errorcode => "modify_project_no_permission", :message => exception.message
+  end
+
+  rescue_from Package::WritePermissionError do |exception|
+    render_error :status => 403, :errorcode => "modify_package_no_permission", :message => exception.message
+  end
+
   rescue_from Suse::Backend::NotFoundError, ActiveRecord::RecordNotFound do |exception|
     render_error message: exception.message, status: 404, errorcode: 'not_found'
   end

programming_language: Ruby
filename: package.rb
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,9 @@ class Package < ActiveRecord::Base
   class SaveError < APIException
     setup "package_save_error"
   end
+  class WritePermissionError < APIException
+    setup "package_write_permission_error"
+  end
   class ReadAccessError < APIException
     setup 'unknown_package', 404, "Unknown package"
   end
@@ -239,6 +242,14 @@ class Package < ActiveRecord::Base
     return self.project.is_locked?
   end
 
+  def check_write_access!
+    return if Rails.env.test? and User.current.nil? # for unit tests
+
+    unless User.current.can_modify_package? self
+      raise WritePermissionError, "No permission to modify package '#{self.name}' for user '#{User.current.login}'"
+    end
+  end
+
   # NOTE: this is no permission check, should it be added ?
   def can_be_deleted?
     # check if other packages have me as devel package
@@ -285,14 +296,17 @@ class Package < ActiveRecord::Base
   end
 
   def add_package_kind( kinds )
+    check_write_access!
     private_set_package_kind( kinds, nil, true )
   end
 
   def set_package_kind( kinds = nil )
+    check_write_access!
     private_set_package_kind( kinds )
   end
 
   def set_package_kind_from_commit( commit )
+    check_write_access!
     private_set_package_kind( nil, commit )
   end
 
@@ -419,6 +433,7 @@ class Package < ActiveRecord::Base
   end
 
   def update_from_xml( xmlhash )
+    check_write_access!
     self.title = xmlhash.value('title')
     self.description = xmlhash.value('description')
     self.bcntsynctag = nil
@@ -616,6 +631,7 @@ class Package < ActiveRecord::Base
   end
 
   def store(opts = {})
+    # no write access check here, since this operation may will disable this permission ...
     @commit_opts = opts
     save!
   end
@@ -649,6 +665,7 @@ class Package < ActiveRecord::Base
   end
 
   def add_user( user, role )
+    check_write_access!
     unless role.kind_of? Role
       role = Role.get_by_title(role)
     end
@@ -669,6 +686,7 @@ class Package < ActiveRecord::Base
   end
 
   def add_group( group, role )
+    check_write_access!
     unless role.kind_of? Role
       role = Role.get_by_title(role)
     end
@@ -933,14 +951,17 @@ class Package < ActiveRecord::Base
   end
 
   def remove_all_persons
+    check_write_access!
     self.package_user_role_relationships.delete_all
   end
 
   def remove_all_groups
+    check_write_access!
     self.package_group_role_relationships.delete_all
   end
 
   def remove_role(what, role)
+    check_write_access!
     if what.kind_of? Group
       rel = self.package_group_role_relationships.where(bs_group_id: what.id)
     else
@@ -954,6 +975,7 @@ class Package < ActiveRecord::Base
   end
 
   def add_role(what, role)
+    check_write_access!
     self.transaction do
       if what.kind_of? Group
         self.package_group_role_relationships.create!(role: role, group: what)

programming_language: Ruby
filename: project.rb
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,9 @@ class Project < ActiveRecord::Base
   class SaveError < APIException
     setup "project_save_error"
   end
+  class WritePermissionError < APIException
+    setup "project_write_permission_error"
+  end
   class ForbiddenError < APIException
     setup("change_project_protection_level", 403,
           "admin rights are required to raise the protection level of a project (it won't be safe anyway)")
@@ -247,6 +250,15 @@ class Project < ActiveRecord::Base
     end
   end
 
+  def check_write_access!
+    return if Rails.env.test? and User.current.nil? # for unit tests
+
+    # the can_create_check is inconsistent with package class check_write_access! check
+    unless User.current.can_modify_project?(self) || User.current.can_create_project?(self.name)
+      raise WritePermissionError, "No permission to modify project '#{self.name}' for user '#{User.current.login}'"
+    end
+  end
+
   def find_linking_projects
       sql =<<-END_SQL
       SELECT prj.*
@@ -289,6 +301,8 @@ class Project < ActiveRecord::Base
   end
 
   def update_from_xml(xmlhash, force=nil)
+    check_write_access!
+
     # check for raising read access permissions, which can't get ensured atm
     unless self.new_record? || self.disabled_for?('access', nil, nil)
       if FlagHelper.xml_disabled_for?(xmlhash, 'access')
@@ -892,6 +906,8 @@ class Project < ActiveRecord::Base
   end
 
   def add_user( user, role )
+    check_write_access!
+
     unless role.kind_of? Role
       role = Role.get_by_title(role)
     end
@@ -912,6 +928,8 @@ class Project < ActiveRecord::Base
   end
 
   def add_group( group, role )
+    check_write_access!
+
     unless role.kind_of? Role
       role = Role.get_by_title(role)
     end
@@ -1523,6 +1541,8 @@ class Project < ActiveRecord::Base
   end
 
   def set_project_type(project_type_name)
+    check_write_access!
+
     mytype = DbProjectType.find_by_name(project_type_name)
     return false unless mytype
     self.type_id = mytype.id
@@ -1535,6 +1555,8 @@ class Project < ActiveRecord::Base
   end
 
   def set_maintenance_project(project)
+    check_write_access!
+
     if project.class == Project
       self.maintenance_project_id = project.id
       self.save!
@@ -1583,6 +1605,11 @@ class Project < ActiveRecord::Base
 
   # called either directly or from delayed job
   def do_project_copy( params )
+    # set user if nil, needed for delayed job in Package model
+    User.current ||= User.find_by_login(params[:user])
+
+    check_write_access!
+
     # copy entire project in the backend
     begin
       path = "/source/#{URI.escape(self.name)}"
@@ -1593,9 +1620,6 @@ class Project < ActiveRecord::Base
       # we need to check results of backend in any case (also timeout error eg)
     end
 
-    # set user if nil, needed for delayed job in Package model
-    User.current ||= User.find_by_login(params[:user])
-
     # restore all package meta data objects in DB
     backend_pkgs = Collection.find :package, :match => "@project='#{self.name}'"
     backend_pkgs.each_package do |package|
@@ -1611,6 +1635,8 @@ class Project < ActiveRecord::Base
   def do_project_release( params )
     User.current ||= User.find_by_login(params[:user])
 
+    check_write_access!
+
     packages.each do |pkg|
       pkg.project.repositories.each do |repo|
         next if params[:repository] and params[:repository] != repo.name
@@ -1647,6 +1673,8 @@ class Project < ActiveRecord::Base
   end
 
   def remove_role(what, role)
+    check_write_access!
+
     if what.kind_of? Group
       rel = self.project_group_role_relationships.where(bs_group_id: what.id)
     else
@@ -1660,6 +1688,8 @@ class Project < ActiveRecord::Base
   end
  
   def add_role(what, role)
+    check_write_access!
+
     self.transaction do
       if what.kind_of? Group
         self.project_group_role_relationships.create!(role: role, group: what)
@@ -1684,6 +1714,8 @@ class Project < ActiveRecord::Base
   end
 
   def update_patchinfo(patchinfo, opts = {})
+    check_write_access!
+
     opts[:enfore_issue_update] ||= false
 
     # collect bugnumbers from diff
@@ -1721,6 +1753,8 @@ class Project < ActiveRecord::Base
   end
 
   def create_patchinfo_from_request(req)
+    check_write_access!
+
     patchinfo = Package.new(:name => "patchinfo", :title => "Patchinfo", :description => "Collected packages for update")
     self.packages << patchinfo
     patchinfo.add_flag("build", "enable", nil, nil)
@@ -1755,6 +1789,7 @@ class Project < ActiveRecord::Base
 
   # updates packages automatically generated in the backend after submitting a product file
   def update_product_autopackages
+    check_write_access!
 
     backend_pkgs = Collection.find :id, :what => 'package', :match => "@project='#{self.name}' and starts-with(@name,'_product:')"
     b_pkg_index = backend_pkgs.each_package.inject(Hash.new) {|hash,elem| hash[elem.name] = elem; hash}

programming_language: Ruby
filename: project_test.rb
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,7 @@ class ProjectTest < ActiveSupport::TestCase
   
   
   def test_add_new_flags_from_xml
+    User.current = users( :Iggy )
     
     #precondition check
     @project.flags.delete_all
@@ -88,6 +89,8 @@ class ProjectTest < ActiveSupport::TestCase
   
   
   def test_delete_flags_through_xml
+    User.current = users( :Iggy )
+
     #check precondition
     assert_equal 2, @project.type_flags('build').size
     assert_equal 2, @project.type_flags('publish').size
@@ -107,6 +110,8 @@ class ProjectTest < ActiveSupport::TestCase
 
     
   def test_store_axml
+    User.current = users( :Iggy )
+
     original = @project.to_axml
 
     #project is given as axml
@@ -131,6 +136,8 @@ class ProjectTest < ActiveSupport::TestCase
   end  
 
   def test_ordering
+    User.current = users( :Iggy )
+
     #project is given as axml
     axml = Xmlhash.parse(
       "<project name='home:Iggy'>
@@ -175,6 +182,7 @@ class ProjectTest < ActiveSupport::TestCase
   end
     
   test "duplicated repos" do
+     User.current = users( :king )
      orig = @project.render_axml
 
      axml = Xmlhash.parse(
@@ -199,6 +207,7 @@ class ProjectTest < ActiveSupport::TestCase
   end
 
   test "duplicated repos with remote" do
+     User.current = users( :Iggy )
      orig = @project.render_axml
 
      xml = <<END
@@ -225,6 +234,7 @@ END
      assert_equal orig, @project.render_axml
   end
   test "not duplicated repos with remote" do
+     User.current = users( :Iggy )
      xml = <<END
 <project name="home:Iggy">
   <title>Iggy"s Home Project</title>
@@ -250,6 +260,7 @@ END
   end
 
   def test_create_maintenance_project_and_maintained_project
+    User.current = users( :king )
     maintenance_project = Project.new(:name => 'Maintenance:Project')
     assert_equal true, maintenance_project.set_project_type('maintenance')
     assert_equal 'maintenance', maintenance_project.project_type()

programming_language: Ruby