Просмотр исходного кода

Merge pull request #2062 from causefx/v2-develop

V2 develop
causefx 1 неделя назад
Родитель
Сommit
6b27515ff3
3 измененных файлов с 130 добавлено и 7 удалено
  1. 101 7
      api/classes/organizr.class.php
  2. 22 0
      api/functions/upgrade-functions.php
  3. 7 0
      js/version.json

+ 101 - 7
api/classes/organizr.class.php

@@ -73,7 +73,7 @@ class Organizr
 
 	// ===================================
 	// Organizr Version
-	public $version = '2.1.4010';
+	public $version = '2.1.5000';
 	// ===================================
 	// Quick php Version check
 	public $minimumPHP = '7.4';
@@ -4031,11 +4031,14 @@ class Organizr
 			$this->setAPIResponse('error', 'Email was not supplied', 422);
 			return false;
 		}
+		if (!$this->config['PHPMAILER-enabled']) {
+			$this->setAPIResponse('error', 'Email functionality is not enabled', 422);
+			return false;
+		}
 		$newPassword = $this->randString(10);
 		$isUser = $this->getUserByEmail($email);
 		if ($isUser) {
 			$this->updateUserPassword($newPassword, $isUser['id']);
-			$this->setAPIResponse('success', 'User password has been reset', 200);
 			$this->setLoggerChannel('User Management');
 			$this->logger->info('User Management Function - User: ' . $isUser['username'] . '\'s password was reset');
 			if ($this->config['PHPMAILER-enabled']) {
@@ -4056,13 +4059,11 @@ class Organizr
 					'body' => $PhpMailer->_phpMailerPluginBuildEmail($emailTemplate),
 				);
 				$PhpMailer->_phpMailerPluginSendEmail($sendEmail);
-				$this->setAPIResponse('success', 'User password has been reset and email has been sent', 200);
 			}
-			return true;
-		} else {
-			$this->setAPIResponse('error', 'User not found', 404);
-			return false;
 		}
+		// Always return the same message to prevent account enumeration
+		$this->setAPIResponse('success', 'If the email exists in our system, a password reset has been sent to the user', 200);
+		return true;
 	}
 
 	public function register($array)
@@ -7460,6 +7461,94 @@ public function youtubeSearch($query)
 		}
 	}
 
+	/**
+	 * Validate that URL is external and not a local/internal resource
+	 * Prevents SSRF attacks by blocking local file access and private IP ranges
+	 * 
+	 * @param string $url The URL to validate
+	 * @return bool True if URL is external and safe, false otherwise
+	 */
+	private function isExternalURL($url)
+	{
+		// Parse the URL
+		$parsedUrl = parse_url($url);
+		
+		if (!$parsedUrl || !isset($parsedUrl['scheme']) || !isset($parsedUrl['host'])) {
+			return false;
+		}
+		
+		// Block file:// and other non-http(s) schemes
+		$scheme = strtolower($parsedUrl['scheme']);
+		if (!in_array($scheme, ['http', 'https'])) {
+			return false;
+		}
+		
+		$host = strtolower($parsedUrl['host']);
+		
+		// Block localhost variations
+		$localhostPatterns = [
+			'localhost',
+			'127.0.0.1',
+			'0.0.0.0',
+			'::1',
+			'0:0:0:0:0:0:0:1'
+		];
+		
+		if (in_array($host, $localhostPatterns)) {
+			return false;
+		}
+		
+		// Resolve hostname to IP if it's not already an IP
+		$ip = $host;
+		if (!filter_var($host, FILTER_VALIDATE_IP)) {
+			$ip = gethostbyname($host);
+			// If gethostbyname fails, it returns the hostname unchanged
+			if ($ip === $host) {
+				// Could not resolve - for security, block unresolvable hosts
+				return false;
+			}
+		}
+		
+		// Block private IP ranges (IPv4)
+		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
+			// Convert IP to long for range checking
+			$ipLong = ip2long($ip);
+			
+			// Private IP ranges:
+			// 10.0.0.0 - 10.255.255.255
+			// 172.16.0.0 - 172.31.255.255
+			// 192.168.0.0 - 192.168.255.255
+			// 169.254.0.0 - 169.254.255.255 (link-local)
+			// 127.0.0.0 - 127.255.255.255 (loopback)
+			$privateRanges = [
+				['10.0.0.0', '10.255.255.255'],
+				['172.16.0.0', '172.31.255.255'],
+				['192.168.0.0', '192.168.255.255'],
+				['169.254.0.0', '169.254.255.255'],
+				['127.0.0.0', '127.255.255.255']
+			];
+			
+			foreach ($privateRanges as $range) {
+				$rangeStart = ip2long($range[0]);
+				$rangeEnd = ip2long($range[1]);
+				if ($ipLong >= $rangeStart && $ipLong <= $rangeEnd) {
+					return false;
+				}
+			}
+		}
+		
+		// Block private/local IPv6 addresses
+		if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
+			// Block IPv6 loopback (::1) and link-local (fe80::/10)
+			if (strpos($ip, '::1') === 0 || strpos($ip, 'fe80:') === 0 || strpos($ip, 'fc00:') === 0 || strpos($ip, 'fd00:') === 0) {
+				return false;
+			}
+		}
+		
+		// URL passed all checks - it's external
+		return true;
+	}
+
 	public function scrapePage($array)
 	{
 		try {
@@ -7470,6 +7559,11 @@ public function youtubeSearch($query)
 				return false;
 			}
 			$url = $this->qualifyURL($url);
+			// Security: Only allow external URLs, block local/internal resources
+			if (!$this->isExternalURL($url)) {
+				$this->setAPIResponse('error', 'Access to local or internal URLs is not allowed', 403);
+				return false;
+			}
 			$data = array(
 				'full_url' => $url,
 				'drill_url' => $this->qualifyURL($url, true)

+ 22 - 0
api/functions/upgrade-functions.php

@@ -99,6 +99,14 @@ trait UpgradeFunctions
 				$this->upgradeToVersion($versionCheck);
 			}
 			// End Upgrade check start for version above
+			// Upgrade check start for version below
+			$versionCheck = '2.1.5000';
+			if ($compare->lessThan($oldVer, $versionCheck)) {
+				$updateDB = false;
+				$oldVer = $versionCheck;
+				$this->upgradeToVersion($versionCheck);
+			}
+			// End Upgrade check start for version above
 			if ($updateDB == true) {
 				//return 'Upgraded Needed - Current Version '.$oldVer.' - New Version: '.$versionCheck;
 				// Upgrade database to latest version
@@ -460,12 +468,26 @@ trait UpgradeFunctions
 				$this->addGroupIdMaxToDatabase();
 				$this->addAddToAdminToDatabase();
 				break;
+			case '2.1.5000':
+				$this->fixGroupOIDC();
+				break;
 		}
 		$this->setLoggerChannel('Upgrade')->notice('Finished upgrade to version ' . $version);
 		$this->setAPIResponse('success', 'Ran update function for version: ' . $version, 200);
 		return true;
 	}
 
+	public function fixGroupOIDC()
+	{
+		$this->updateConfig(array('oidcDefaultGroupId' => (string) $this->config['oidcDefaultGroupId']));
+		$this->logger->info(
+			'Updated OIDC default group id to string type', 
+			[ 
+				'oldValue' => $this->config['oidcDefaultGroupId'],
+				'newValue' => (string) $this->config['oidcDefaultGroupId']
+			]);
+	}
+
 	public function removeOldCacheFolder()
 	{
 		$folder = $this->root . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . 'images' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR;

+ 7 - 0
js/version.json

@@ -663,5 +663,12 @@
     "new": "",
     "fixed": "fix: change oidcDefaultGroupId to string type in default.php",
     "notes": ""
+  },
+  "2.1.5000": {
+    "date": "2026-05-19 23:17",
+    "title": "Weekly Update",
+    "new": "feat: added URL validation to prevent SSRF attacks by blocking local/internal resources",
+    "fixed": "fix:settings page not loading after OIDC|fix: enhance password reset functionality to prevent account enumeration",
+    "notes": ""
   }
 }