CVE-2020-24881 (PHP, published 2020-11-02T21:15Z)
Description: value: SSRF exists in osTicket before 1.14.3, where an attacker can add malicious file to server or perform port scanning.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-918 \u2014 Server-Side Request Forgery (SSRF)
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.
Repository: https://github.com/osTicket/osTicket
Files changed:
include/class.config.php
include/class.format.php
include/class.thread.php
include/i18n/en_US/config.yaml
include/i18n/en_US/help/tips/settings.ticket.yaml
include/staff/settings-tickets.inc.php
Commit message: 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`).


Vulnerable code:
            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]);
            return Format::clickableurls($this->body);

Fixed code:
        'allow_external_images' => 1,
    function allowExternalImages() {
        return ($this->get('allow_external_images'));
    }

            'allow_external_images'=>isset($vars['allow_external_images'])?1:0,
        global $cfg;

        // Exclude external images?
        $exclude = !$cfg->allowExternalImages();
        // Allowed image extensions
        $allowed = array('gif', 'png', 'jpg', 'jpeg');

            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 '';
    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;
    }

        // Ensure valid external images
        $body = Format::stripExternalImages($body);

            return Format::clickableurls(Format::stripExternalImages($this->body, true));
    allow_external_images: 1
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.

        <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>