CVE-2020-24881 (published 2020-11-02T21:15Z)

SSRF exists in osTicket before 1.14.3, where an attacker can add malicious file to server or perform port scanning.

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

Weakness information:
cwe_id: CWE-918
cwe_name: Server-Side Request Forgery (SSRF)
description: The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.
extended_description: By providing URLs to unexpected hosts or ports, attackers can make it appear that the server is sending the request, possibly bypassing access controls such as firewalls that prevent the attackers from accessing the URLs directly. The server can be used as a proxy to conduct port scanning of hosts in internal networks, use other URLs such as that can access documents on the system (using file://), or use other protocols such as gopher:// or tftp://, which may provide greater control over the contents of requests.
         

Fix information:
repo_url: https://github.com/osTicket/osTicket
commit_details: repo_url: https://github.com/osTicket/osTicket
author: JediKev
author_date: 2020-08-18 10:21:35-05:00
msg: ssrf: External Inline Images

This mitigates an SSRF security vulnerability reported by [Talat
Mehmood](https://twitter.com/Blackbatsecuri1) where we do not check if the
src URL for external inline images contain a valid image extension when
displaying. This means if someone puts something like `<img
src="mymalicioussite.com/badscript.js">` in the body of their message, when
someone clicks Show Images or when the ticket is printed the server will
call the src URL to get the "image" data for display. This is a problem as
this could load malicious code/scripts in the browser. This adds a new
setting called `Allow External Images` and if Disabled we will not allow any
external images in Threads. If this setting is Enabled (default), we will
only allow `<img>` src URLs with valid image extensions
(`png`,`jpg`,`jpeg`,`gif`).
repository: repo_url: https://github.com/osTicket/osTicket
repo_name: osTicket/osTicket
description: The osTicket open source ticketing system official project repository, for versions 1.8 and later
repo_language: PHP
owner: osTicket
file_changes: filename: class.config.php
change_type: ModificationType.MODIFY
diff: @@ -230,6 +230,7 @@ class OsticketConfig extends Config {
         'max_open_tickets' => 0,
         'files_req_auth' => 1,
         'force_https' => '',
+        'allow_external_images' => 1,
     );
 
     function __construct($section=null) {
@@ -1035,6 +1036,10 @@ class OsticketConfig extends Config {
         return $this->get('require_topic_to_close');
     }
 
+    function allowExternalImages() {
+        return ($this->get('allow_external_images'));
+    }
+
     function getDefaultTicketQueueId() {
         return $this->get('default_ticket_queue', 1);
     }
@@ -1417,6 +1422,7 @@ class OsticketConfig extends Config {
             'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0,
             'ticket_lock' => $vars['ticket_lock'],
             'default_ticket_queue'=>$vars['default_ticket_queue'],
+            'allow_external_images'=>isset($vars['allow_external_images'])?1:0,
         ));
     }
 

programming_language: PHP
filename: class.format.php
change_type: ModificationType.MODIFY
diff: @@ -411,13 +411,29 @@ class Format {
 
     //Format text for display..
     function display($text, $inline_images=true, $balance=true) {
+        global $cfg;
+
+        // Exclude external images?
+        $exclude = !$cfg->allowExternalImages();
+        // Allowed image extensions
+        $allowed = array('gif', 'png', 'jpg', 'jpeg');
+
         // Make showing offsite images optional
         $text = preg_replace_callback('/<img ([^>]*)(src="http[^"]+")([^>]*)\/>/',
-            function($match) {
-                // Drop embedded classes -- they don't refer to ours
-                $match = preg_replace('/class="[^"]*"/', '', $match);
-                return sprintf('<span %s class="non-local-image" data-%s %s></span>',
-                    $match[1], $match[2], $match[3]);
+            function($match) use ($exclude, $allowed) {
+                $m = array();
+                // Split the src URL and get the extension
+                preg_match('/src="([^"]+)"/', $match[2], $m);
+                $url = explode('.', explode('?', $m[1])[0]);
+                $ext = preg_split('/[^A-Za-z]/', end($url))[0];
+
+                if (!$exclude && in_array($ext, $allowed)) {
+                    // Drop embedded classes -- they don't refer to ours
+                    $match = preg_replace('/class="[^"]*"/', '', $match);
+                    return sprintf('<span %s class="non-local-image" data-%s %s></span>',
+                        $match[1], $match[2], $match[3]);
+                } else
+                    return '';
             },
             $text);
 
@@ -433,6 +449,29 @@ class Format {
         return $text;
     }
 
+    function stripExternalImages($input, $display=false) {
+        global $cfg;
+
+        // Allowed Inline External Image Extensions
+        $allowed = array('gif', 'png', 'jpg', 'jpeg');
+        $exclude = !$cfg->allowExternalImages();
+
+        $input = preg_replace_callback('/<img ([^>]*)(src="([^"]+)")([^>]*)\/?>/',
+            function($match) use ($allowed, $exclude, $display) {
+                // Split the src URL and get the extension
+                $url = explode('.', explode('?', $match[3])[0]);
+                $ext = preg_split('/[^A-Za-z]/', end($url))[0];
+
+                if (($exclude && $display) || !in_array($ext, $allowed))
+                    return '';
+                else
+                    return $match[0];
+            },
+            $input);
+
+        return $input;
+    }
+
     function striptags($var, $decode=true) {
 
         if(is_array($var))

programming_language: PHP
filename: class.thread.php
change_type: ModificationType.MODIFY
diff: @@ -1647,6 +1647,9 @@ implements TemplateVariable {
         if (!($body = Format::strip_emoticons($vars['body']->getClean())))
             $body = '-'; //Special tag used to signify empty message as stored.
 
+        // Ensure valid external images
+        $body = Format::stripExternalImages($body);
+
         $poster = $vars['poster'];
         if ($poster && is_object($poster))
             $poster = (string) $poster;
@@ -2890,7 +2893,7 @@ class HtmlThreadEntryBody extends ThreadEntryBody {
         case 'email':
             return $this->body;
         case 'pdf':
-            return Format::clickableurls($this->body);
+            return Format::clickableurls(Format::stripExternalImages($this->body, true));
         default:
             return Format::display($this->body, true, !$this->options['balanced']);
         }

programming_language: PHP
filename: config.yaml
change_type: ModificationType.MODIFY
diff: @@ -80,6 +80,7 @@ core:
     ticket_number_format: '######'
     ticket_sequence_id: 0
     queue_bucket_counts: 0
+    allow_external_images: 1
     task_number_format: '#'
     task_sequence_id: 2
     log_level: 2

programming_language: Ruby
filename: settings.ticket.yaml
change_type: ModificationType.MODIFY
diff: @@ -124,6 +124,15 @@ require_topic_to_close:
     content: >
         If Enabled, a Ticket must have a Help Topic in order to be Closed by an Agent
 
+allow_external_images:
+    title: Allow External Images
+    content: >
+        If Enabled, the system will allow external inline images that have a valid image
+        extension (.png, .jpg, .jpeg, .gif). If Disabled, the system will exclude
+        any external inline images. One caveat to note, is if the setting is Disabled we
+        will still store external inline images that have a valid image extension in case
+        the setting is re-enabled in the future.
+
 assigned_tickets:
     title: Assigned Tickets
     content: >

programming_language: Markdown
filename: settings-tickets.inc.php
change_type: ModificationType.MODIFY
diff: @@ -234,6 +234,13 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <?php echo __('Enable'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#require_topic_to_close"></i>
             </td>
         </tr>
+        <tr>
+            <td><?php echo __('Allow External Images'); ?>:</td>
+            <td>
+                <input type="checkbox" name="allow_external_images" <?php echo $config['allow_external_images']?'checked="checked"':''; ?>>
+                <?php echo __('Enable'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#allow_external_images"></i>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Attachments');?></b>:  <?php echo __('Size and maximum uploads setting mainly apply to web tickets.');?></em>

programming_language: PHP