[TikiWiki-commits] [Git][tikiwiki/tiki][master] [FIX] User Avatars: uploading custom user profile picture leading to error 500 (WSoD)

"Jonny Bradley \(@jonnybradley\) via TikiWiki-cvs" <[email protected]> Thu, 02 Jul 2026 11:22:52 +0000
Newsgroups gmane.comp.cms.tiki.cvs
Message-ID <6a464a0c3d83a_38199d94621c1@gitlab-sidekiq-low-urgency-cpu-bound-v2-6d66df59d-p8w5p.mail>

Jonny Bradley pushed to branch master at Tiki Wiki CMS Groupware / Tiki


Commits:
7d5b3197 by Elifeleti Mukisa Dan at 2026-07-02T11:05:00+00:00
[FIX] User Avatars: uploading custom user profile picture leading to error 500 (WSoD)
---
* [FIX] Avatar upload was let to error 500 WSoD

* This reverts commit 165e29644df751b4193569f53c1c6fa438517943.

* [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path

See merge request tikiwiki/tiki!10631

- - - - -


6 changed files:

- doc/devtools/process_user_logins.php
- lib/avatarlib.php
- lib/core/Services/User/Controller.php
- lib/socialnetworkslib.php
- lib/trackers/trackerlib.php
- tiki-pick_avatar.php


Changes:

=====================================
doc/devtools/process_user_logins.php
=====================================
@@ -138,7 +138,7 @@ function processUsers(): void
                         if (strlen($name) > 80) {
                             $name = substr($name, 0, 80);
                         }
-                        $avatarlib->set_avatar_from_url($wrapper->getReadableFile(), $login, $name);
+                        $avatarlib->setAvatarFromFile($wrapper->getReadableFile(), $login, $name);
 
                         echo " done\n";
                     }


=====================================
lib/avatarlib.php
=====================================
@@ -22,16 +22,54 @@ if (str_contains($_SERVER['SCRIPT_NAME'], basename(__FILE__))) {
 class AvatarLib extends TikiLib
 {
     /**
-     * sets the avatar from a given image file's URL
+     * sets the avatar from a given remote image file's URL
      *
-     * @param string $url        location of the file
+     * @param string $url        location of the remote file
      * @param string $userwatch  user the avatar is for
      * @param string $name       original name of the file
      *
      * @throws Exception
      */
-
     final public function set_avatar_from_url(string $url, string $userwatch = '', string $name = ''): void
+    {
+        // Validate URL to prevent SSRF via user-supplied avatar URLs
+        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
+        if (! $ssrf->isUrlAllowed($url)) {
+            throw new \Exception($this->getDisallowedAvatarUrlMessage($url));
+        }
+
+        $this->setAvatarFromSource($url, $userwatch, $name, true);
+    }
+
+    /**
+     * sets the avatar from a local image file path
+     *
+     * @param string $file       local file path
+     * @param string $userwatch  user the avatar is for
+     * @param string $name       original name of the file
+     *
+     * @throws Exception
+     */
+    final public function setAvatarFromFile(string $file, string $userwatch = '', string $name = ''): void
+    {
+        if (! is_file($file) || ! is_readable($file)) {
+            throw new \Exception(tr('Avatar file is not readable'));
+        }
+
+        $this->setAvatarFromSource($file, $userwatch, $name, false);
+    }
+
+    private function getDisallowedAvatarUrlMessage(string $url): string
+    {
+        $host = parse_url($url, PHP_URL_HOST);
+        if ($host && Perms::get()->admin) {
+            return tr('Avatar URL host "%0" is not allowed. Review the SSRF whitelist in Security Admin.', $host);
+        }
+
+        return tr('Avatar URL is not allowed. Ask a site administrator to review the SSRF whitelist.');
+    }
+
+    private function setAvatarFromSource(string $source, string $userwatch, string $name, bool $isRemoteUrl): void
     {
         global $user, $prefs;
 
@@ -47,23 +85,26 @@ class AvatarLib extends TikiLib
             $userwatch = $user;
         }
 
-        // Validate URL to prevent SSRF via user-supplied avatar URLs
-        $ssrf = \Tiki\Security\SsrfLib::fromPrefs();
-        if (! $ssrf->isUrlAllowed($url)) {
-            throw new \Exception('Avatar URL is not allowed');
+        $data = @file_get_contents($source);
+        if ($data === false) {
+            throw new \Exception($isRemoteUrl ? tr('Avatar URL could not be read.') : tr('Avatar file could not be read.'));
         }
 
-        $data = file_get_contents($url);
-        list($iwidth, $iheight, $itype, $iattr) = getimagesize($url);
+        $imageInfo = @getimagesize($source);
+        if ($imageInfo === false) {
+            throw new \Exception($isRemoteUrl ? tr('Avatar URL does not point to a valid image.') : tr('Avatar file is not a valid image.'));
+        }
+        list($iwidth, $iheight, $itype, $iattr) = $imageInfo;
         $itype = image_type_to_mime_type($itype);
 
         // Get proper file size of image
-        $imgdata = get_headers($url, true);
-        if (isset($imgdata['Content-Length'])) {
-            # Return file size
-            $size = (int)$imgdata['Content-Length'];
-        } else {
-            $size = strlen($data);
+        $size = strlen($data);
+        if ($isRemoteUrl) {
+            $imgdata = @get_headers($source, true);
+            if (isset($imgdata['Content-Length'])) {
+                # Return file size
+                $size = (int)$imgdata['Content-Length'];
+            }
         }
 
         // Store full-size file gallery image if that is required
@@ -85,6 +126,9 @@ class AvatarLib extends TikiLib
         } else {
             if (function_exists('imagecreatefromstring') && (! str_contains($itype, 'gif'))) {
                 $img = imagecreatefromstring($data);
+                if ($img === false) {
+                    throw new \Exception(tr('Avatar image could not be processed.'));
+                }
                 $size_x = imagesx($img);
                 $size_y = imagesy($img);
                 /* if the square crop is set, crop the image before resizing */
@@ -94,6 +138,9 @@ class AvatarLib extends TikiLib
                     $offset_y = ($size_y - $crop_size) / 2;
                     $crop_array = ['x' => $offset_x , 'y' => $offset_y, 'width' => $crop_size, 'height' => $crop_size];
                     $img = imagecrop($img, $crop_array);
+                    if ($img === false) {
+                        throw new \Exception(tr('Avatar image could not be cropped.'));
+                    }
                     $size_x = $size_y = $crop_size;
                 }
                 if ($size_x > $size_y) {
@@ -109,24 +156,43 @@ class AvatarLib extends TikiLib
                 if ($ty > $size_y) {
                     $ty = $size_y;
                 }
-                if (chkgd2()) {
-                    $t = imagecreatetruecolor($tw, $ty);
-                    // trick to have a transparent background for png instead of black
-                    imagesavealpha($t, true);
-                    $trans_colour = imagecolorallocatealpha($t, 0, 0, 0, 127);
-                    imagefill($t, 0, 0, $trans_colour);
-                    imagecopyresampled($t, $img, 0, 0, 0, 0, $tw, $ty, $size_x, $size_y);
-                } else {
+                if (! chkgd2()) {
                     // TODO ImageGalleryRemoval23.x - replace imagick if no GD
+                    throw new \Exception(tr('Avatar image could not be resized because GD is not available.'));
+                }
+                $t = imagecreatetruecolor($tw, $ty);
+                if ($t === false) {
+                    throw new \Exception(tr('Avatar image could not be resized.'));
+                }
+                // trick to have a transparent background for png instead of black
+                imagesavealpha($t, true);
+                $trans_colour = imagecolorallocatealpha($t, 0, 0, 0, 127);
+                imagefill($t, 0, 0, $trans_colour);
+                if (! imagecopyresampled($t, $img, 0, 0, 0, 0, $tw, $ty, $size_x, $size_y)) {
+                    throw new \Exception(tr('Avatar image could not be resized.'));
                 }
                 // CHECK IF THIS TEMP IS WRITEABLE OR CHANGE THE PATH TO A WRITEABLE DIRECTORY
                 $tmpfname = tempnam($prefs['tmpDir'], "TMPIMG");
-                imagepng($t, $tmpfname);
+                if ($tmpfname === false || ! imagepng($t, $tmpfname)) {
+                    throw new \Exception(tr('Avatar image could not be saved to a temporary file.'));
+                }
                 // Now read the information
                 $fp = fopen($tmpfname, "rb");
-                $t_data = fread($fp, filesize($tmpfname));
+                if ($fp === false) {
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
+                $tmpSize = filesize($tmpfname);
+                if ($tmpSize === false) {
+                    fclose($fp);
+                    unlink($tmpfname);
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
+                $t_data = fread($fp, $tmpSize);
                 fclose($fp);
                 unlink($tmpfname);
+                if ($t_data === false) {
+                    throw new \Exception(tr('Avatar image temporary file could not be read.'));
+                }
                 $t_type = 'image/png';
                 $userprefslib->set_user_avatar($userwatch, 'u', '', $name, $size, $t_type, $t_data);
             } else {


=====================================
lib/core/Services/User/Controller.php
=====================================
@@ -1121,11 +1121,22 @@ class Services_User_Controller
                 throw new Services_Exception($errormsg, 400);
             }
             $name = $_FILES['userfile']['name'];
+            $filegallib = TikiLib::lib('filegal');
+            try {
+                $filegallib->assertUploadedFileIsSafe($_FILES['userfile']['tmp_name'], $_FILES['userfile']['name']);
+            } catch (Exception $e) {
+                throw new Services_Exception($e->getMessage(), 403);
+            }
+
             /**
              * @var $avatarlib AvatarLib
              */
             $avatarlib = TikiLib::lib('avatar');
-            $avatarlib->set_avatar_from_url($_FILES['userfile']['tmp_name'], $userwatch, $name);
+            try {
+                $avatarlib->setAvatarFromFile($_FILES['userfile']['tmp_name'], $userwatch, $name);
+            } catch (Exception $e) {
+                throw new Services_Exception($e->getMessage(), 400);
+            }
             return true;
         } else {
             return [


=====================================
lib/socialnetworkslib.php
=====================================
@@ -356,7 +356,11 @@ class SocialNetworksLib extends LogsLib
         if ($prefs['feature_userPreferences'] == 'y') {
             $fb_avatar = json_decode($this->facebookGraph('', 'me/picture', ['type' => 'square', 'width' => '480', 'redirect' => '0','access_token' => $access_token], false, 'GET'));
             $avatarlib = TikiLib::lib('avatar');
-            $avatarlib->set_avatar_from_url($fb_avatar->data->url, $user);
+            try {
+                $avatarlib->set_avatar_from_url($fb_avatar->data->url, $user);
+            } catch (Exception $e) {
+                Feedback::error($e->getMessage());
+            }
         }
 
         return $user;
@@ -550,7 +554,11 @@ class SocialNetworksLib extends LogsLib
                     }
                     if ($displayImage) {
                         $avatarlib = TikiLib::lib('avatar');
-                        $avatarlib->set_avatar_from_url($displayImage, $user);
+                        try {
+                            $avatarlib->set_avatar_from_url($displayImage, $user);
+                        } catch (Exception $e) {
+                            Feedback::error($e->getMessage());
+                        }
                     }
                 }
             } else {


=====================================
lib/trackers/trackerlib.php
=====================================
@@ -2248,7 +2248,7 @@ class TrackerLib extends TikiLib
                             try {
                                 $filegallib->assertUploadedFileIsSafe($_FILES[$filekey]['tmp_name'], $_FILES[$filekey]['name']);
                                 $avatarlib = TikiLib::lib('avatar');
-                                $avatarlib->set_avatar_from_url($_FILES[$filekey]['tmp_name'], $trackersync_user, $_FILES[$filekey]['name']);
+                                $avatarlib->setAvatarFromFile($_FILES[$filekey]['tmp_name'], $trackersync_user, $_FILES[$filekey]['name']);
                             } catch (Exception $e) {
                                 Feedback::error($e->getMessage());
                             }


=====================================
tiki-pick_avatar.php
=====================================
@@ -56,15 +56,19 @@ if (isset($_FILES['userfile1'])) {
         }
 
         $avatarlib = TikiLib::lib('avatar');
-        $avatarlib->set_avatar_from_url($_FILES['userfile1']['tmp_name'], $userwatch, $name);
+        try {
+            $avatarlib->setAvatarFromFile($_FILES['userfile1']['tmp_name'], $userwatch, $name);
 
-        /* redirect to prevent re-submit on page reload */
-        if ($tiki_p_admin == 'y' && $user !== $userwatch) {
-            header('Location: tiki-pick_avatar.php?view_user=' . $userwatch);
-        } else {
-            header('Location: tiki-pick_avatar.php');
+            /* redirect to prevent re-submit on page reload */
+            if ($tiki_p_admin == 'y' && $user !== $userwatch) {
+                header('Location: tiki-pick_avatar.php?view_user=' . $userwatch);
+            } else {
+                header('Location: tiki-pick_avatar.php');
+            }
+            exit;
+        } catch (Exception $e) {
+            Feedback::error($e->getMessage());
         }
-        exit;
     } else {
         Feedback::error($tikilib->uploaded_file_error($_FILES['userfile1']['error']));
     }



View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/7d5b3197608b75a90aaa5afca38d588b8f62b3f9

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/7d5b3197608b75a90aaa5afca38d588b8f62b3f9
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help

_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs