CVE-2023-22481 (published 2023-03-06T18:15Z)

FreshRSS is a self-hosted RSS feed aggregator. When using the greader API, the provided password is logged in clear in `users/_/log_api.txt` in the case where the authentication fails. The issues occurs in `authorizationToUser()` in `greader.php`. If there is an issue with the request or the credentials, `unauthorized()` or `badRequest()` is called. Both these functions are printing the return of `debugInfo()` in the logs.  `debugInfo()` will return the content of the request. By default, this will be saved in `users/_/log_api.txt` and if the const `COPY_LOG_TO_SYSLOG` is true, in syslogs as well. Exploiting this issue requires having access to logs produced by FreshRSS. Using the information from the logs, a malicious individual could get users' API keys (would be displayed if the users fills in a bad username) or passwords.

CVSS v3 base score: 5.5, MEDIUM (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-532
cwe_name: Insertion of Sensitive Information into Log File
description: Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
extended_description: 
            While logging all information may be helpful during development stages, it is important that logging levels be set appropriately before a product ships so that sensitive user data and system information are not accidentally exposed to potential attackers.
            Different log files may be produced and stored for:
            
               Server log files (e.g. server.log).  This can give information on whatever application left the file. Usually this can give full path names and system information, and sometimes usernames and passwords.
               log files that are used for debugging
            
         
         

Fix information:
repo_url: https://github.com/FreshRSS/FreshRSS
commit_details: repo_url: https://github.com/FreshRSS/FreshRSS
author: Alexandre Alapetite
author_date: 2023-01-11 23:27:14+01:00
msg: API avoid logging passwords (#5001)

* API avoid logging passwords
* Strip passwords and tokens from API logs
* Only log failed requests information when in debug mode

* Remove debug SHA

* Clean also Apache logs

* Better comments

* Redact also token parameters

* shfmt

* Simplify whitespace

* redacted
repository: repo_url: https://github.com/FreshRSS/FreshRSS
repo_name: FreshRSS/FreshRSS
description: A free, self-hostable news aggregator…
repo_language: PHP
owner: FreshRSS
file_changes: filename: FreshRSS.Apache.conf
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@ DocumentRoot /var/www/FreshRSS/p/
 RemoteIPHeader X-Forwarded-For
 RemoteIPTrustedProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
 LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy
-CustomLog /dev/stdout combined_proxy
+CustomLog "|/var/www/FreshRSS/cli/sensitive-log.sh" combined_proxy
 ErrorLog /dev/stderr
 AllowEncodedSlashes On
 ServerTokens OS

programming_language: Markdown
filename: sensitive-log.sh
change_type: ModificationType.ADD
diff: @@ -0,0 +1,9 @@
+#!/bin/sh
+# Strips sensitive passwords from (Apache) logs
+
+# For e.g. GNU systems such as Debian
+# N.B.: `sed -u` is not available in BusyBox and without it there are buffering delays (even with stdbuf)
+sed -Eu 's/([?&])(Passwd|token)=[^& \t]+/\1\2=redacted/ig' 2>/dev/null ||
+
+	# For systems with gawk (not available by default in Docker of Debian or Alpine) or with BuzyBox such as Alpine
+	$(which gawk || which awk) -v IGNORECASE=1 '{ print gensub(/([?&])(Passwd|token)=[^& \t]+/, "\\1\\2=redacted", "g") }'

programming_language: Shell
filename: lib_rss.php
change_type: ModificationType.MODIFY
diff: @@ -223,6 +223,31 @@ function html_only_entity_decode($text): string {
 	return $text == '' ? '' : strtr($text, $htmlEntitiesOnly);
 }
 
+/**
+ * Remove passwords in FreshRSS logs.
+ * See also ../cli/sensitive-log.sh for Web server logs.
+ * @param array<string,mixed>|string $log
+ * @return array<string,mixed>|string
+ */
+function sensitive_log($log) {
+	if (is_array($log)) {
+		foreach ($log as $k => $v) {
+			if (in_array($k, ['api_key', 'Passwd', 'T'])) {
+				$log[$k] = '██';
+			} else {
+				$log[$k] = sensitive_log($v);
+			}
+		}
+	} elseif (is_string($log)) {
+		$log = preg_replace([
+				'/\b(auth=.*?\/)[^&]+/i',
+				'/\b(Passwd=)[^&]+/i',
+				'/\b(Authorization)[^&]+/i',
+			], '$1█', $log);
+	}
+	return $log;
+}
+
 /**
  * @param array<string,mixed> $attributes
  */

programming_language: PHP
filename: fever.php
change_type: ModificationType.MODIFY
diff: @@ -18,7 +18,8 @@ FreshRSS_Context::initSystem();
 
 // check if API is enabled globally
 if (!FreshRSS_Context::$system_conf->api_enabled) {
-	Minz_Log::warning('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('Fever API: service unavailable!');
+	Minz_Log::debug('Fever API: serviceUnavailable() ' . debugInfo(), API_LOG);
 	header('HTTP/1.1 503 Service Unavailable');
 	header('Content-Type: text/plain; charset=UTF-8');
 	die('Service Unavailable!');
@@ -45,16 +46,16 @@ function debugInfo() {
 		}
 	}
 	global $ORIGINAL_INPUT;
-	return print_r(
-		array(
+	$log = sensitive_log([
 			'date' => date('c'),
 			'headers' => $ALL_HEADERS,
 			'_SERVER' => $_SERVER,
 			'_GET' => $_GET,
 			'_POST' => $_POST,
 			'_COOKIE' => $_COOKIE,
-			'INPUT' => $ORIGINAL_INPUT
-		), true);
+			'INPUT' => $ORIGINAL_INPUT,
+		]);
+	return print_r($log, true);
 }
 
 //Minz_Log::debug('----------------------------------------------------------------', API_LOG);

programming_language: PHP
filename: greader.php
change_type: ModificationType.MODIFY
diff: @@ -97,27 +97,29 @@ function debugInfo() {
 		}
 	}
 	global $ORIGINAL_INPUT;
-	return print_r(
-		array(
+	$log = sensitive_log([
 			'date' => date('c'),
 			'headers' => $ALL_HEADERS,
 			'_SERVER' => $_SERVER,
 			'_GET' => $_GET,
 			'_POST' => $_POST,
 			'_COOKIE' => $_COOKIE,
-			'INPUT' => $ORIGINAL_INPUT
-		), true);
+			'INPUT' => $ORIGINAL_INPUT,
+		]);
+	return print_r($log, true);
 }
 
 function badRequest() {
-	Minz_Log::warning('badRequest() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('GReader API: ' . __METHOD__, API_LOG);
+	Minz_Log::debug('badRequest() ' . debugInfo(), API_LOG);
 	header('HTTP/1.1 400 Bad Request');
 	header('Content-Type: text/plain; charset=UTF-8');
 	die('Bad Request!');
 }
 
 function unauthorized() {
-	Minz_Log::warning('unauthorized() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('GReader API: ' . __METHOD__, API_LOG);
+	Minz_Log::debug('unauthorized() ' . debugInfo(), API_LOG);
 	header('HTTP/1.1 401 Unauthorized');
 	header('Content-Type: text/plain; charset=UTF-8');
 	header('Google-Bad-Token: true');
@@ -125,21 +127,24 @@ function unauthorized() {
 }
 
 function notImplemented() {
-	Minz_Log::warning('notImplemented() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('GReader API: ' . __METHOD__, API_LOG);
+	Minz_Log::debug('notImplemented() ' . debugInfo(), API_LOG);
 	header('HTTP/1.1 501 Not Implemented');
 	header('Content-Type: text/plain; charset=UTF-8');
 	die('Not Implemented!');
 }
 
 function serviceUnavailable() {
-	Minz_Log::warning('serviceUnavailable() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('GReader API: ' . __METHOD__, API_LOG);
+	Minz_Log::debug('serviceUnavailable() ' . debugInfo(), API_LOG);
 	header('HTTP/1.1 503 Service Unavailable');
 	header('Content-Type: text/plain; charset=UTF-8');
 	die('Service Unavailable!');
 }
 
 function checkCompatibility() {
-	Minz_Log::warning('checkCompatibility() ' . debugInfo(), API_LOG);
+	Minz_Log::warning('GReader API: ' . __METHOD__, API_LOG);
+	Minz_Log::debug('checkCompatibility() ' . debugInfo(), API_LOG);
 	header('Content-Type: text/plain; charset=UTF-8');
 	if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) {
 		die('FAIL 64-bit or GMP extension! Wrong PHP configuration.');
@@ -172,8 +177,7 @@ function authorizationToUser() {
 				if ($headerAuthX[1] === sha1(FreshRSS_Context::$system_conf->salt . $user . FreshRSS_Context::$user_conf->apiPasswordHash)) {
 					return $user;
 				} else {
-					Minz_Log::warning('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1], API_LOG);
-					Minz_Log::warning('Invalid API authorisation for user ' . $user . ': ' . $headerAuthX[1]);
+					Minz_Log::warning('Invalid API authorisation for user ' . $user);
 					unauthorized();
 				}
 			} else {

programming_language: PHP