CVE-2020-29159 (published 2020-12-28T08:15Z)

An issue was discovered in Zammad before 3.5.1. The default signup Role (for newly created Users) can be a privileged Role, if configured by an admin. This behvaior was unintended.

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

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/zammad/zammad
commit_details: repo_url: https://github.com/zammad/zammad
author: Rolf Schmidt
author_date: 2020-10-07 14:05:07+02:00
msg: Maintenance: Provide allow_signup column to define the signup permissions for roles and disable new permissions by default as signup permission.
repository: repo_url: https://github.com/zammad/zammad
repo_name: zammad/zammad
description: Zammad is a web based open source helpdesk/customer support system
repo_language: Ruby
owner: zammad
file_changes: filename: role.rb
change_type: ModificationType.MODIFY
diff: @@ -222,13 +222,12 @@ returns
   end
 
   def check_default_at_signup_permissions
-    all_permissions = Permission.all.pluck(:id)
-    admin_permissions = Permission.where('name LIKE ? OR name = ?', 'admin%', 'ticket.agent').pluck(:id) # admin.*/ticket.agent permissions
-    normal_permissions = (all_permissions - admin_permissions) | (admin_permissions - all_permissions) # all other permissions besides admin.*/ticket.agent
-    return true if default_at_signup != true # means if default_at_signup = false, no need further checks
-    return true if self.permission_ids.all? { |i| normal_permissions.include? i } # allow user to choose only normal permissions
+    return true if !default_at_signup
 
-    raise Exceptions::UnprocessableEntity, 'Cannot set default at signup when role has admin or ticket.agent permissions.'
+    forbidden_permissions = permissions.reject(&:allow_signup)
+    return true if forbidden_permissions.blank?
+
+    raise Exceptions::UnprocessableEntity, "Cannot set default at signup when role has #{forbidden_permissions.join(', ')} permissions."
   end
 
 end

programming_language: Ruby
filename: 20120101000001_create_base.rb
change_type: ModificationType.MODIFY
diff: @@ -128,11 +128,12 @@ class CreateBase < ActiveRecord::Migration[4.2]
     add_foreign_key :roles, :users, column: :updated_by_id
 
     create_table :permissions do |t|
-      t.string :name,                   limit: 255, null: false
-      t.string :note,                   limit: 500, null: true
-      t.string :preferences,            limit: 10_000, null: true
-      t.boolean :active,                               null: false, default: true
-      t.timestamps limit: 3, null: false
+      t.string :name,          limit: 255, null: false
+      t.string :note,          limit: 500, null: true
+      t.string :preferences,   limit: 10_000, null: true
+      t.boolean :active,       null: false, default: true
+      t.boolean :allow_signup, null: false, default: false
+      t.timestamps limit: 3,   null: false
     end
     add_index :permissions, [:name], unique: true
 

programming_language: Ruby
filename: 20201005113317_role_signup_column.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,31 @@
+class RoleSignupColumn < ActiveRecord::Migration[5.2]
+  def change
+
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    add_column :permissions, :allow_signup, :boolean, null: false, default: false
+
+    signup_permissions = [
+      'user_preferences',
+      'user_preferences.password',
+      'user_preferences.notifications',
+      'user_preferences.access_token',
+      'user_preferences.language',
+      'user_preferences.linked_accounts',
+      'user_preferences.device',
+      'user_preferences.avatar',
+      'user_preferences.calendar',
+      'user_preferences.out_of_office',
+      'ticket.customer',
+    ]
+
+    Permission.where(name: signup_permissions).update(allow_signup: true)
+
+    Role.where(default_at_signup: true).find_each do |role|
+      role.permissions.where.not(name: signup_permissions).find_each do |permission|
+        role.permission_revoke(permission.name)
+      end
+    end
+  end
+end

programming_language: Ruby
filename: permissions.rb
change_type: ModificationType.MODIFY
diff: @@ -263,75 +263,85 @@ Permission.create_if_not_exists(
   },
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences',
-  note:        'User Preferences',
-  preferences: {},
+  name:         'user_preferences',
+  note:         'User Preferences',
+  preferences:  {},
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.password',
-  note:        'Change %s',
-  preferences: {
+  name:         'user_preferences.password',
+  note:         'Change %s',
+  preferences:  {
     translations: ['Password']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.notifications',
-  note:        'Manage %s',
-  preferences: {
+  name:         'user_preferences.notifications',
+  note:         'Manage %s',
+  preferences:  {
     translations: ['Notifications'],
     required:     ['ticket.agent'],
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.access_token',
-  note:        'Manage %s',
-  preferences: {
+  name:         'user_preferences.access_token',
+  note:         'Manage %s',
+  preferences:  {
     translations: ['Token Access']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.language',
-  note:        'Change %s',
-  preferences: {
+  name:         'user_preferences.language',
+  note:         'Change %s',
+  preferences:  {
     translations: ['Language']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.linked_accounts',
-  note:        'Manage %s',
-  preferences: {
+  name:         'user_preferences.linked_accounts',
+  note:         'Manage %s',
+  preferences:  {
     translations: ['Linked Accounts']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.device',
-  note:        'Manage %s',
-  preferences: {
+  name:         'user_preferences.device',
+  note:         'Manage %s',
+  preferences:  {
     translations: ['Devices']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.avatar',
-  note:        'Manage %s',
-  preferences: {
+  name:         'user_preferences.avatar',
+  note:         'Manage %s',
+  preferences:  {
     translations: ['Avatar']
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.calendar',
-  note:        'Access to %s',
-  preferences: {
+  name:         'user_preferences.calendar',
+  note:         'Access to %s',
+  preferences:  {
     translations: ['Calendars'],
     required:     ['ticket.agent'],
   },
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
-  name:        'user_preferences.out_of_office',
-  note:        'Change %s',
-  preferences: {
+  name:         'user_preferences.out_of_office',
+  note:         'Change %s',
+  preferences:  {
     translations: ['Out of Office'],
     required:     ['ticket.agent'],
   },
+  allow_signup: true,
 )
 
 Permission.create_if_not_exists(
@@ -354,9 +364,10 @@ Permission.create_if_not_exists(
   },
 )
 Permission.create_if_not_exists(
-  name:        'ticket.customer',
-  note:        'Access to Customer Tickets based on current_user and organization',
-  preferences: {},
+  name:         'ticket.customer',
+  note:         'Access to Customer Tickets based on current_user and organization',
+  preferences:  {},
+  allow_signup: true,
 )
 Permission.create_if_not_exists(
   name:        'chat',

programming_language: JavaScript
filename: role_signup_column_spec.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,30 @@
+require 'rails_helper'
+
+RSpec.describe RoleSignupColumn, type: :db_migration, db_strategy: :reset do
+  context 'when a role contains signup permissions' do
+    let!(:role) do
+      role = create(:role)
+      role.permission_grant('user_preferences.password')
+      role.permission_grant('ticket.agent')
+      role.update_column(:default_at_signup, true)
+      role
+    end
+
+    before do
+      without_column(:permissions, column: :allow_signup)
+      migrate
+    end
+
+    it 'has password permission' do
+      expect(role.reload.permissions.map(&:name)).to include('user_preferences.password')
+    end
+
+    it 'has no agent permission' do
+      expect(role.reload.permissions.map(&:name)).not_to include('ticket.agent')
+    end
+
+    it 'has permission with allow_signup set correctly' do
+      expect(Permission.find_by(name: 'user_preferences.password').allow_signup).to be true
+    end
+  end
+end

programming_language: Ruby