CVE-2023-46730: Server-Side Request Forgery (SSRF) (CWE-918)
value: Group-Office is an enterprise CRM and groupware tool. In affected versions there is full Server-Side Request Forgery (SSRF) vulnerability in the /api/upload.php endpoint. The /api/upload.php endpoint does not filter URLs which allows a malicious user to cause the server to make resource requests to untrusted domains. Note that protocols like file:// can also be used to access the server disk. The request result (on success) can then be retrieved using /api/download.php. This issue has been addressed in versions 6.8.15, 6.7.54, and 6.6.177. Users are advised to upgrade. There are no known workarounds for this vulnerability.
CWE 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.
CVSS v3 base score: 8.8
Published: 2023-11-07T18:15Z
Repository: https://github.com/Intermesh/groupoffice
Language: PHP
Files changed: www/api/upload.php, www/go/core/model/Settings.php, www/go/core/views/extjs3/form/ImageField.js, www/go/core/webclient/Extjs3.php
Commit date: 2023-11-03 10:50:50 +0100
Commit message: Fix security issue GHSA-vw6c-h82w-mvfv

Diff: diff --git a/www/api/upload.php b/www/api/upload.php
index 8d0b95254..e54bc16b8 100644
--- a/www/api/upload.php
+++ b/www/api/upload.php
@@ -20,60 +20,42 @@ try {
 	if (!App::get()->getAuthState()->isAuthenticated()) {
 		Response::get()->setStatus(401, 'Created');
 		throw new \go\core\http\Exception(401);
 	}
 
-	if (isset($_GET['url'])) {
-		$tmpFile = \go\core\fs\File::tempFile('tmp');
+	//raise max_execution_time for calculating hash of large files
+	ini_set('max_execution_time', 300);
 
-		try {
-			$httpClient = new Client();
-			$response = $httpClient->download($_GET['url'], $tmpFile);
+	$filename = Request::get()->getHeader('X-File-Name');
+	$filename = Request::headerDecode($filename);
+	$tmpFile = \go\core\fs\File::tempFile($filename);
 
-			$blob = Blob::fromTmp($tmpFile);
-			$blob->name = $response['name'];
-			$blob->type = $response['type'];
-		} catch(\Exception $e) {
-			throw new \Exception("Failed to download from given URL " .  $_GET['url']);
-		}
+	$input = fopen('php://input', "r");
+	$fp = $tmpFile->open("w+");
+	while ($data = fread($input, 4096)) { // 4kb at the time
+		fwrite($fp, $data);
+	}
+	fclose($fp);
+	fclose($input);
+
+	if (\go\core\jmap\Capabilities::get()->maxSizeUpload && $tmpFile->getSize() > \go\core\jmap\Capabilities::get()->maxSizeUpload) {
+		$tmpFile->delete();
+		Response::get()->setStatus(413);
+		Response::get()->output([
+			"error" => "File exceeds maximum size of " . \go\core\jmap\Capabilities::get()->maxSizeUpload . " bytes"
+		]);
+		exit();
+	}
 
+	$blob = Blob::fromTmp($tmpFile);
+	$blob->name = $filename;
+	if (Request::get()->getHeader('X-File-LastModified') == null) {
+		$blob->modifiedAt = new \go\core\util\DateTime();
 	} else {
-
-		//raise max_execution_time for calculating hash of large files
-		ini_set('max_execution_time', 300);
-
-		$filename = Request::get()->getHeader('X-File-Name');
-		$filename = Request::headerDecode($filename);
-		$tmpFile = \go\core\fs\File::tempFile($filename);
-
-		$input = fopen('php://input', "r");
-		$fp = $tmpFile->open("w+");
-		while ($data = fread($input, 4096)) { // 4kb at the time
-			fwrite($fp, $data);
-		}
-		fclose($fp);
-		fclose($input);
-
-		if (\go\core\jmap\Capabilities::get()->maxSizeUpload && $tmpFile->getSize() > \go\core\jmap\Capabilities::get()->maxSizeUpload) {
-			$tmpFile->delete();
-			Response::get()->setStatus(413);
-			Response::get()->output([
-				"error" => "File exceeds maximum size of " . \go\core\jmap\Capabilities::get()->maxSizeUpload . " bytes"
-			]);
-			exit();
-		}
-
-		$blob = Blob::fromTmp($tmpFile);
-		$blob->name = $filename;
-		if (Request::get()->getHeader('X-File-LastModified') == null) {
-			$blob->modifiedAt = new \go\core\util\DateTime();
-		} else {
-			$blob->modifiedAt = new \go\core\util\DateTime('@' . Request::get()->getHeader('X-File-LastModified'));
-		}
+		$blob->modifiedAt = new \go\core\util\DateTime('@' . Request::get()->getHeader('X-File-LastModified'));
 	}
 
-
 	if ($blob->save()) {
 		Response::get()->setStatus(201, 'Created');
 		$response = $blob->toArray();
 		$response['blobId'] = $blob->id; //deprecated
 		Response::get()->output($response);
diff --git a/www/go/core/model/Settings.php b/www/go/core/model/Settings.php
index fc73ec843..e97ade368 100644
--- a/www/go/core/model/Settings.php
+++ b/www/go/core/model/Settings.php
@@ -304,12 +304,19 @@ class Settings extends core\Settings {
 	
 	/**
 	 * The full URL to Group-Office. With trailing /.
 	 * 
 	 * eg. https://my.groupoffice.net/
-	 * 
-	 * @var string 
+	 *
+	 * Alternatively to generate a URL based on the request you can use:
+	 *
+	 * @example
+	 * ```
+	 * Extjs3::get()->getBaseUrl();
+	 * ```
+	 *
+	 * @var string
 	 */
 	public $URL;
 
 	/**
 	 * @var string
diff --git a/www/go/core/views/extjs3/form/ImageField.js b/www/go/core/views/extjs3/form/ImageField.js
index b0d491017..a4b2759f8 100644
--- a/www/go/core/views/extjs3/form/ImageField.js
+++ b/www/go/core/views/extjs3/form/ImageField.js
@@ -77,35 +77,10 @@ go.form.ImageField = Ext.extend(Ext.BoxComponent, {
 							scope: this
 						}
 					});
 				},
 				scope: this
-			}, {
-				iconCls: 'ic-link',
-				text: t("From URL"),
-				handler: function () {
-					Ext.MessageBox.prompt(t("Set Image From URL"), t("Enter URL"), function(btn, url) {
-
-						
-						if(btn != "ok" || !url) {
-							return;
-						}
-
-
-						Ext.Ajax.request({
-							url: go.User.uploadUrl + "?url=" + encodeURIComponent(url),
-							method: "GET",
-							success: function(response) {
-								data = Ext.decode(response.responseText);
-								this.setValue(data.blobId);								
-							},
-							scope: this
-						});
-
-					}, this);
-				},
-				scope: this
 			},
 			{
 				iconCls: 'ic-delete',
 				text: t("Clear"),
 				handler: function () {
diff --git a/www/go/core/webclient/Extjs3.php b/www/go/core/webclient/Extjs3.php
index 47e34e199..509c16f98 100644
--- a/www/go/core/webclient/Extjs3.php
+++ b/www/go/core/webclient/Extjs3.php
@@ -177,10 +177,12 @@ class Extjs3 {
 	private $baseUrl;
 
 	/**
 	 * Get URL to webclient
 	 *
+	 * eg. https://groupoffice.example.com/groupoffice/
+	 *
 	 * @return string
 	 */
 	public function getBaseUrl() {
 
 		if(isset($this->baseUrl)) {

Vulnerable code: 	if (isset($_GET['url'])) {
		$tmpFile = \go\core\fs\File::tempFile('tmp');
		try {
			$httpClient = new Client();
			$response = $httpClient->download($_GET['url'], $tmpFile);
			$blob = Blob::fromTmp($tmpFile);
			$blob->name = $response['name'];
			$blob->type = $response['type'];
		} catch(\Exception $e) {
			throw new \Exception("Failed to download from given URL " .  $_GET['url']);
		}

		//raise max_execution_time for calculating hash of large files
		ini_set('max_execution_time', 300);

		$filename = Request::get()->getHeader('X-File-Name');
		$filename = Request::headerDecode($filename);
		$tmpFile = \go\core\fs\File::tempFile($filename);

		$input = fopen('php://input', "r");
		$fp = $tmpFile->open("w+");
		while ($data = fread($input, 4096)) { // 4kb at the time
			fwrite($fp, $data);
		}
		fclose($fp);
		fclose($input);

		if (\go\core\jmap\Capabilities::get()->maxSizeUpload && $tmpFile->getSize() > \go\core\jmap\Capabilities::get()->maxSizeUpload) {
			$tmpFile->delete();
			Response::get()->setStatus(413);
			Response::get()->output([
				"error" => "File exceeds maximum size of " . \go\core\jmap\Capabilities::get()->maxSizeUpload . " bytes"
			]);
			exit();
		}

		$blob = Blob::fromTmp($tmpFile);
		$blob->name = $filename;
		if (Request::get()->getHeader('X-File-LastModified') == null) {
			$blob->modifiedAt = new \go\core\util\DateTime();
		} else {
			$blob->modifiedAt = new \go\core\util\DateTime('@' . Request::get()->getHeader('X-File-LastModified'));
		}

	 * 
	 * @var string 
			}, {
				iconCls: 'ic-link',
				text: t("From URL"),
				handler: function () {
					Ext.MessageBox.prompt(t("Set Image From URL"), t("Enter URL"), function(btn, url) {

						
						if(btn != "ok" || !url) {
							return;
						}


						Ext.Ajax.request({
							url: go.User.uploadUrl + "?url=" + encodeURIComponent(url),
							method: "GET",
							success: function(response) {
								data = Ext.decode(response.responseText);
								this.setValue(data.blobId);								
							},
							scope: this
						});

					}, this);
				},
				scope: this
Fixed code: 	//raise max_execution_time for calculating hash of large files
	ini_set('max_execution_time', 300);
	$filename = Request::get()->getHeader('X-File-Name');
	$filename = Request::headerDecode($filename);
	$tmpFile = \go\core\fs\File::tempFile($filename);
	$input = fopen('php://input', "r");
	$fp = $tmpFile->open("w+");
	while ($data = fread($input, 4096)) { // 4kb at the time
		fwrite($fp, $data);
	}
	fclose($fp);
	fclose($input);

	if (\go\core\jmap\Capabilities::get()->maxSizeUpload && $tmpFile->getSize() > \go\core\jmap\Capabilities::get()->maxSizeUpload) {
		$tmpFile->delete();
		Response::get()->setStatus(413);
		Response::get()->output([
			"error" => "File exceeds maximum size of " . \go\core\jmap\Capabilities::get()->maxSizeUpload . " bytes"
		]);
		exit();
	}
	$blob = Blob::fromTmp($tmpFile);
	$blob->name = $filename;
	if (Request::get()->getHeader('X-File-LastModified') == null) {
		$blob->modifiedAt = new \go\core\util\DateTime();
		$blob->modifiedAt = new \go\core\util\DateTime('@' . Request::get()->getHeader('X-File-LastModified'));
	 *
	 * Alternatively to generate a URL based on the request you can use:
	 *
	 * @example
	 * ```
	 * Extjs3::get()->getBaseUrl();
	 * ```
	 *
	 * @var string
	 * eg. https://groupoffice.example.com/groupoffice/
	 *