[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