CVE-2021-43403: Insufficient Information (NVD-CWE-noinfo)
value: An issue was discovered in FusionPBX before 4.5.30. The log_viewer.php Log View page allows an authenticated user to choose an arbitrary filename for download (i.e., not necessarily freeswitch.log in the intended directory).
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 6.5
Published: 2022-09-29T03:15Z
Repository: https://github.com/fusionpbx/fusionpbx
Language: PHP
Files changed: app/log_viewer/log_viewer.php
Commit date: 2021-11-03 12:33:28 -0600
Commit message: Add better log filename validation.

This was needed after the a previous pull request that allowed selecting a log file to download.
Diff: diff --git a/app/log_viewer/log_viewer.php b/app/log_viewer/log_viewer.php
index 1fae25ec91..25acb819e5 100644
--- a/app/log_viewer/log_viewer.php
+++ b/app/log_viewer/log_viewer.php
@@ -15,11 +15,11 @@
 
 	The Original Code is FusionPBX
 
 	The Initial Developer of the Original Code is
 	Mark J Crane <markjcrane@fusionpbx.com>
-	Portions created by the Initial Developer are Copyright (C) 2008-2019
+	Portions created by the Initial Developer are Copyright (C) 2008-2021
 	the Initial Developer. All Rights Reserved.
 
 	Contributor(s):
 	Mark J Crane <markjcrane@fusionpbx.com>
 	James Rose <james.o.rose@gmail.com>
@@ -42,36 +42,61 @@
 //add multi-lingual support
 	$language = new text;
 	$text = $language->get();
 
 //set a default line number value (off)
-	if (!isset($_POST['line_number']) || $_POST['line_number'] == '') { $_POST['line_number'] = 0; }
+	if (!isset($_POST['line_number']) || $_POST['line_number'] == '') {
+		$_POST['line_number'] = 0; 
+	}
 
 //set a default ordinal (descending)
-	if (!isset($_POST['sort']) || $_POST['sort'] == '') { $_POST['sort'] = "asc"; }
+	if (!isset($_POST['sort']) || $_POST['sort'] == '') { 
+		$_POST['sort'] = "asc";
+	}
 
 //set a default file size
-	if (!isset($_POST['size']) || strlen($_POST['size']) == 0) { $_POST['size'] = "32"; }
+	if (!isset($_POST['size']) || strlen($_POST['size']) == 0) { 
+		$_POST['size'] = "32";
+	}
 
 //set a default filter
-	if (!isset($_POST['filter'])) { $_POST['filter'] = ""; }	
+	if (!isset($_POST['filter'])) {
+		$_POST['filter'] = '';
+	}
 
 //set default default log file
-	if (!isset($_POST['log_file']) || substr($_POST['log_file'],0,14) != "freeswitch.log") { $_POST['log_file'] = "freeswitch.log"; }	
+	if (isset($_POST['log_file'])) {
+		$approved_files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
+		foreach($approved_files as $approved_file) {
+			if ($approved_file == $_SESSION['switch']['log']['dir'].'/'.$_POST['log_file']) {
+				$log_file = $approved_file;
+			}
+		}
+	}
+	else {
+		$log_file = $_SESSION['switch']['log']['dir'].'/freeswitch.log';
+	}
 
 //download the log
 	if (permission_exists('log_download')) {
-		if (isset($_GET['n']) && substr($_GET['n'],0,14) == "freeswitch.log") {
-			$dir = $_SESSION['switch']['log']['dir'];
-			$filename = $_GET['n'];
-			session_cache_limiter('public');
-			$fd = fopen($dir."/".$filename, "rb");
-			header("Content-Type: binary/octet-stream");
-			header("Content-Length: " . filesize($tmp."/".$filename));
-			header('Content-Disposition: attachment; filename="'.$filename.'"');
-			fpassthru($fd);
-			exit;
+		if (isset($_GET['n'])) {
+			if (isset($filename)) { unset($filename); }
+			$approved_files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
+			foreach($approved_files as $approved_file) {
+				if ($approved_file == $_SESSION['switch']['log']['dir'].'/'.$_GET['n']) {
+					$filename = $approved_file;
+				}
+			}
+			if (isset($filename) && file_exists($filename)) {
+				session_cache_limiter('public');
+				$fd = fopen($filename, "rb");
+				header("Content-Type: binary/octet-stream");
+				header("Content-Length: " . filesize($filename));
+				header('Content-Disposition: attachment; filename="'.basename($filename).'"');
+				fpassthru($fd);
+				exit;
+			}
 		}
 	}
 
 //include the header
 	$document['title'] = $text['title-log_viewer'];
@@ -81,23 +106,23 @@
 	echo "<div class='action_bar' id='action_bar'>\n";
 	echo "	<div class='heading'><b>".$text['title-log_viewer']."</b></div>\n";
 	echo "	<div class='actions'>\n";
 	echo 		"<form name='frm' id='frm' class='inline' method='post'>\n";
 	echo "			".$text['label-log_file']." <select name='log_file' class='formfld' style='width: 150px; margin-right: 20px;'>";
-	$files = scandir($_SESSION['switch']['log']['dir']);
-	foreach($files as $file) if (substr($file,0,14) == "freeswitch.log") {
-			$selected = ($file == $_POST['log_file']) ? "selected='selected'" : "";
-			echo "			<option value='".$file."'".$selected.">".$file."</option>";
+	$files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
+	foreach($files as $file) {
+		$selected = ($file == $log_file) ? "selected='selected'" : "";
+		echo "			<option value='".basename($file)."'".$selected.">".basename($file)."</option>";
 	}
 	echo "			</select>\n";
 	echo 		$text['label-filter']." <input type='text' name='filter' class='formfld' style='width: 150px; text-align: center; margin-right: 20px;' value=\"".escape($_POST['filter'])."\" onclick='this.select();'>";
 	echo 		"<label style='margin-right: 20px; margin-top: 4px;'><input type='checkbox' name='line_number' id='line_number' value='1' ".($_POST['line_number'] == 1 ? 'checked' : null)."> ".$text['label-line_number']."</label>";
 	echo 		"<label style='margin-right: 20px; margin-top: 4px;'><input type='checkbox' name='sort' id='sort' value='desc' ".($_POST['sort'] == 'desc' ? 'checked' : null)."> ".$text['label-sort']."</label>";
 	echo 		$text['label-display']." <input type='text' class='formfld' style='width: 50px; text-align: center;' name='size' value=\"".escape($_POST['size'])."\" onclick='this.select();'> ".$text['label-size'];
 	echo button::create(['type'=>'submit','label'=>$text['button-update'],'icon'=>$_SESSION['theme']['button_icon_save'],'style'=>'margin-left: 15px;','name'=>'submit']);
 	if (permission_exists('log_download')) {
-		echo button::create(['type'=>'button','label'=>$text['button-download'],'icon'=>$_SESSION['theme']['button_icon_download'],'style'=>'margin-left: 15px;','link'=>'log_viewer.php?a=download&n='.$_POST['log_file']]);
+		echo button::create(['type'=>'button','label'=>$text['button-download'],'icon'=>$_SESSION['theme']['button_icon_download'],'style'=>'margin-left: 15px;','link'=>'log_viewer.php?a=download&n='.basename($log_file)]);
 	}
 	echo 		"</form>\n";
 	echo "	</div>\n";
 	echo "	<div style='clear: both;'></div>\n";
 	echo "</div>\n";
@@ -113,13 +138,10 @@
 		$user_file_size = '0';
 		$default_color = '#fff';
 		$default_type = 'normal';
 		$default_font = 'monospace';
 		$default_file_size = '512000';
-		if (substr($_POST['log_file'],0,14) == "freeswitch.log") {
-			$log_file = $_SESSION['switch']['log']['dir']."/".$_POST['log_file'];
-		}
 
 		//put the color matches here...
 		$array_filter[0]['pattern'] = '[NOTICE]';
 		$array_filter[0]['color'] = 'cyan';
 		$array_filter[0]['type'] = 'normal';
@@ -294,6 +316,6 @@
 	echo "</table>\n";
 
 //include the footer
 	require_once "resources/footer.php";
 
-?>
\ No newline at end of file
+?>

Vulnerable code: 	Portions created by the Initial Developer are Copyright (C) 2008-2019
	if (!isset($_POST['line_number']) || $_POST['line_number'] == '') { $_POST['line_number'] = 0; }
	if (!isset($_POST['sort']) || $_POST['sort'] == '') { $_POST['sort'] = "asc"; }
	if (!isset($_POST['size']) || strlen($_POST['size']) == 0) { $_POST['size'] = "32"; }
	if (!isset($_POST['filter'])) { $_POST['filter'] = ""; }	
	if (!isset($_POST['log_file']) || substr($_POST['log_file'],0,14) != "freeswitch.log") { $_POST['log_file'] = "freeswitch.log"; }	
		if (isset($_GET['n']) && substr($_GET['n'],0,14) == "freeswitch.log") {
			$dir = $_SESSION['switch']['log']['dir'];
			$filename = $_GET['n'];
			session_cache_limiter('public');
			$fd = fopen($dir."/".$filename, "rb");
			header("Content-Type: binary/octet-stream");
			header("Content-Length: " . filesize($tmp."/".$filename));
			header('Content-Disposition: attachment; filename="'.$filename.'"');
			fpassthru($fd);
			exit;
	$files = scandir($_SESSION['switch']['log']['dir']);
	foreach($files as $file) if (substr($file,0,14) == "freeswitch.log") {
			$selected = ($file == $_POST['log_file']) ? "selected='selected'" : "";
			echo "			<option value='".$file."'".$selected.">".$file."</option>";
		echo button::create(['type'=>'button','label'=>$text['button-download'],'icon'=>$_SESSION['theme']['button_icon_download'],'style'=>'margin-left: 15px;','link'=>'log_viewer.php?a=download&n='.$_POST['log_file']]);
		if (substr($_POST['log_file'],0,14) == "freeswitch.log") {
			$log_file = $_SESSION['switch']['log']['dir']."/".$_POST['log_file'];
		}
?>
Fixed code: 	Portions created by the Initial Developer are Copyright (C) 2008-2021
	if (!isset($_POST['line_number']) || $_POST['line_number'] == '') {
		$_POST['line_number'] = 0; 
	}
	if (!isset($_POST['sort']) || $_POST['sort'] == '') { 
		$_POST['sort'] = "asc";
	}
	if (!isset($_POST['size']) || strlen($_POST['size']) == 0) { 
		$_POST['size'] = "32";
	}
	if (!isset($_POST['filter'])) {
		$_POST['filter'] = '';
	}
	if (isset($_POST['log_file'])) {
		$approved_files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
		foreach($approved_files as $approved_file) {
			if ($approved_file == $_SESSION['switch']['log']['dir'].'/'.$_POST['log_file']) {
				$log_file = $approved_file;
			}
		}
	}
	else {
		$log_file = $_SESSION['switch']['log']['dir'].'/freeswitch.log';
	}
		if (isset($_GET['n'])) {
			if (isset($filename)) { unset($filename); }
			$approved_files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
			foreach($approved_files as $approved_file) {
				if ($approved_file == $_SESSION['switch']['log']['dir'].'/'.$_GET['n']) {
					$filename = $approved_file;
				}
			}
			if (isset($filename) && file_exists($filename)) {
				session_cache_limiter('public');
				$fd = fopen($filename, "rb");
				header("Content-Type: binary/octet-stream");
				header("Content-Length: " . filesize($filename));
				header('Content-Disposition: attachment; filename="'.basename($filename).'"');
				fpassthru($fd);
				exit;
			}
	$files = glob($_SESSION['switch']['log']['dir'].'/freeswitch.log*');
	foreach($files as $file) {
		$selected = ($file == $log_file) ? "selected='selected'" : "";
		echo "			<option value='".basename($file)."'".$selected.">".basename($file)."</option>";
		echo button::create(['type'=>'button','label'=>$text['button-download'],'icon'=>$_SESSION['theme']['button_icon_download'],'style'=>'margin-left: 15px;','link'=>'log_viewer.php?a=download&n='.basename($log_file)]);
?>