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

An issue was discovered in Zammad before 3.5.1. A REST API call allows an attacker to change Ticket Article data in a way that defeats auditing.

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

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

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-08 09:19:37+02:00
msg: Maintenance: Sync Article REST API capabilities with the ones of the frontend.
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: ticket_articles_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -101,11 +101,18 @@ class TicketArticlesController < ApplicationController
     article = Ticket::Article.find(params[:id])
     authorize!(article)
 
-    clean_params = Ticket::Article.association_name_to_id_convert(params)
-    clean_params = Ticket::Article.param_cleanup(clean_params, true)
-
-    # only apply preferences changes (keep not updated keys/values)
-    clean_params = article.param_preferences_merge(clean_params)
+    # only update internal and highlight info
+    clean_params = {}
+    if !params[:internal].nil?
+      clean_params[:internal] = params[:internal]
+    end
+    if params.dig(:preferences, :highlight).present?
+      clean_params = article.param_preferences_merge(clean_params.merge(
+                                                       preferences: {
+                                                         highlight: params[:preferences][:highlight].to_s
+                                                       }
+                                                     ))
+    end
 
     article.update!(clean_params)
 

programming_language: Ruby
filename: article_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -118,8 +118,10 @@ AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
         content_type: 'text/plain',
         body:         'some body',
         type:         'note',
+        internal:     false,
         preferences:  {
           some_key1: 123,
+          highlight: '123',
         },
       }
       post '/api/v1/ticket_articles', params: params, as: :json
@@ -127,28 +129,34 @@ AAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO
       expect(json_response).to be_a_kind_of(Hash)
       expect(json_response['subject']).to be_nil
       expect(json_response['body']).to eq('some body')
+      expect(json_response['internal']).to eq(false)
       expect(json_response['content_type']).to eq('text/plain')
       expect(json_response['updated_by_id']).to eq(agent.id)
       expect(json_response['created_by_id']).to eq(agent.id)
       expect(json_response['preferences']['some_key1']).to eq(123)
+      expect(json_response['preferences']['highlight']).to eq('123')
       expect(ticket.articles.count).to eq(5)
 
       params = {
         body:        'some body 2',
+        internal:    true,
         preferences: {
           some_key2: 'abc',
+          highlight: '234',
         },
       }
       put "/api/v1/ticket_articles/#{json_response['id']}", params: params, as: :json
       expect(response).to have_http_status(:ok)
       expect(json_response).to be_a_kind_of(Hash)
       expect(json_response['subject']).to be_nil
-      expect(json_response['body']).to eq('some body 2')
+      expect(json_response['body']).not_to eq('some body 2')
+      expect(json_response['internal']).to eq(true)
       expect(json_response['content_type']).to eq('text/plain')
       expect(json_response['updated_by_id']).to eq(agent.id)
       expect(json_response['created_by_id']).to eq(agent.id)
       expect(json_response['preferences']['some_key1']).to eq(123)
-      expect(json_response['preferences']['some_key2']).to eq('abc')
+      expect(json_response['preferences']['some_key2']).not_to eq('abc')
+      expect(json_response['preferences']['highlight']).to eq('234')
 
     end
 

programming_language: Ruby
filename: ticket_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -902,7 +902,7 @@ RSpec.describe 'Ticket', type: :request do
       expect(json_response).to be_a_kind_of(Hash)
       expect(json_response['ticket_id']).to eq(ticket.id)
       expect(json_response['from']).to eq(%("Tickets Agent via #{ticket_group.email_address.realname}" <#{ticket_group.email_address.email}>))
-      expect(json_response['subject']).to eq('new subject')
+      expect(json_response['subject']).not_to eq('new subject')
       expect(json_response['body']).to eq('some body')
       expect(json_response['content_type']).to eq('text/plain')
       expect(json_response['internal']).to eq(true)
@@ -991,7 +991,7 @@ RSpec.describe 'Ticket', type: :request do
       expect(json_response).to be_a_kind_of(Hash)
       expect(json_response['ticket_id']).to eq(ticket.id)
       expect(json_response['from']).to eq('Tickets Admin')
-      expect(json_response['subject']).to eq('new subject')
+      expect(json_response['subject']).not_to eq('new subject')
       expect(json_response['body']).to eq('some body')
       expect(json_response['content_type']).to eq('text/plain')
       expect(json_response['internal']).to eq(true)

programming_language: Ruby