diff --git a/dashboard2/.DS_Store b/dashboard2/.DS_Store new file mode 100644 index 0000000..e1d60c0 Binary files /dev/null and b/dashboard2/.DS_Store differ diff --git a/dashboard2/changes.txt b/dashboard2/changes.txt index 68bc80f..17f8c4a 100644 --- a/dashboard2/changes.txt +++ b/dashboard2/changes.txt @@ -1,3 +1,28 @@ +xlx db v2.3.8 + +SECURITY UPDATE - XSS Vulnerability Patches and Security Enhancements +- "functions.php" added SafeOutput() and SafeOutputAttr() for XSS protection + added GenerateCSRFToken() and ValidateCSRFToken() for CSRF protection +- "index.php" added session_start() for CSRF token support + added SafeOutput() to all $_GET['show'] outputs + added input whitelist validation for $_GET['show'] parameter + changed file permission from 777 to 600 for hash file (security hardening) + added SafeOutputAttr() to all meta tag outputs + added SafeOutput() to contact email output + improved error messages to prevent information disclosure +- "users.php" added CSRF token validation for all POST requests + added CSRF tokens to both filter forms + added input validation with regex for callsign filter (alphanumeric, dash, asterisk only) + added input validation with regex for module filter (single letter A-Z only) + added SafeOutput() and SafeOutputAttr() to all user data outputs + added SafeOutput() to all callsign, suffix, via, peer, and module outputs +- "repeaters.php" added SafeOutput() to all node callsign, suffix, protocol, and module outputs + added SafeOutput() to all IP address outputs +- "peers.php" added SafeOutput() and SafeOutputAttr() to peer name and URL outputs + added SafeOutput() to protocol, module, and IP address outputs +- "reflectors.php" added SafeOutput() and SafeOutputAttr() to reflector name, country, comment, and URL outputs +- "class.reflector.php" added URL validation in CallHome() method to prevent remote file inclusion attacks + xlx db v2.3.1 - "config.inc.php" $CallingHome['InterlinkFile'] added @@ -16,7 +41,7 @@ xlx db v2.2.2 This version is a major release with voluntary self-registration feature build in. You need to edit the conf.inc.php to your needs. -On the first run your personal hash to access the database is place in the server’s /tmp folder. +On the first run your personal hash to access the database is place in the server�s /tmp folder. Take care to make a backup of this file because this folder is cleaned up after a server reboot. This version is a major release @@ -44,7 +69,7 @@ xlx db v2.1.4 - "class.reflector.php" improved the flag search - "country.csv" added serveral prefixes -- "flags" added Puerto Ricco and Åland Islands +- "flags" added Puerto Ricco and �land Islands xlx db v2.1.3 diff --git a/dashboard2/index.php b/dashboard2/index.php index 25fea68..bff4404 100644 --- a/dashboard2/index.php +++ b/dashboard2/index.php @@ -1,4 +1,5 @@ '); @fclose($Ressource); - @exec("chmod 777 " . $CallingHome['HashFile']); + @exec("chmod 600 " . $CallingHome['HashFile']); $CallHomeNow = true; } } else { @@ -79,12 +80,11 @@ if ($CallingHome['Active']) { - - - - - - + + + + + <?php echo $Reflector->GetReflectorName(); ?> Reflector Dashboard @@ -113,8 +113,8 @@ if ($CallingHome['Active']) { if (($_SERVER['REQUEST_METHOD'] === 'POST') || isset($_GET['do'])) { echo ' document.location.href = "./index.php'; - if (isset($_GET['show'])) { - echo '?show=' . $_GET['show']; + if (isset($_GET['show']) && $_GET['show'] !== '') { + echo '?show=' . SafeOutput($_GET['show']); } echo '";'; } else { @@ -194,6 +194,15 @@ if ($CallingHome['Active']) { } } + // Whitelist allowed values + if (!isset($_GET['show'])) { + $_GET['show'] = ''; + } + $allowed_shows = ['users', 'repeaters', 'liveircddb', 'peers', 'reflectors', '']; + if (!in_array($_GET['show'], $allowed_shows, true)) { + $_GET['show'] = ''; + } + switch ($_GET['show']) { case 'users' : require_once("./pgs/users.php"); @@ -222,7 +231,7 @@ if ($CallingHome['Active']) { diff --git a/dashboard2/pgs/class.reflector.php b/dashboard2/pgs/class.reflector.php index 2d29483..db07d87 100644 --- a/dashboard2/pgs/class.reflector.php +++ b/dashboard2/pgs/class.reflector.php @@ -419,6 +419,10 @@ class xReflector { } public function CallHome() { + // Validate URL before making request + if (!filter_var($this->CallingHomeServerURL, FILTER_VALIDATE_URL)) { + return false; + } $xml = ' CallingHome'.$this->ReflectorXML.$this->InterlinkXML; $p = @stream_context_create(array('http' => array('header' => "Content-type: application/x-www-form-urlencoded\r\n", diff --git a/dashboard2/pgs/config.inc.php b/dashboard2/pgs/config.inc.php index b17f3fb..3b83f7a 100644 --- a/dashboard2/pgs/config.inc.php +++ b/dashboard2/pgs/config.inc.php @@ -16,7 +16,7 @@ $PageOptions = array(); $PageOptions['ContactEmail'] = 'your_email'; // Support E-Mail address -$PageOptions['DashboardVersion'] = '2.3.7'; // Dashboard Version +$PageOptions['DashboardVersion'] = '2.3.8'; // Dashboard Version $PageOptions['PageRefreshActive'] = true; // Activate automatic refresh $PageOptions['PageRefreshDelay'] = '10000'; // Page refresh time in miliseconds diff --git a/dashboard2/pgs/country.csv b/dashboard2/pgs/country.csv old mode 100755 new mode 100644 diff --git a/dashboard2/pgs/functions.php b/dashboard2/pgs/functions.php index a90af8c..7ce5b08 100644 --- a/dashboard2/pgs/functions.php +++ b/dashboard2/pgs/functions.php @@ -59,4 +59,30 @@ function CreateCode ($laenge) { return $out; } +function SafeOutput($string, $encoding = 'UTF-8') { + return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $encoding); +} + +function SafeOutputAttr($string, $encoding = 'UTF-8') { + // Extra safe for attributes + return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $encoding); +} + +function GenerateCSRFToken() { + if (!isset($_SESSION)) { + session_start(); + } + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); + } + return $_SESSION['csrf_token']; +} + +function ValidateCSRFToken($token) { + if (!isset($_SESSION)) { + session_start(); + } + return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); +} + ?> diff --git a/dashboard2/pgs/peers.php b/dashboard2/pgs/peers.php index 1f45c13..27292d7 100644 --- a/dashboard2/pgs/peers.php +++ b/dashboard2/pgs/peers.php @@ -44,8 +44,8 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) { '.($i+1).''; - $Name = $Reflector->Peers[$i]->GetCallSign(); - $URL = ''; + $Name = $Reflector->Peers[$i]->GetCallSign(); + $URL = ''; for ($j=1;$jGetElement($Reflectors[$j], "name")) { @@ -53,15 +53,15 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) { } } if ($Result && (trim($URL) != "")) { - echo ''.$Name.''; + echo '' . SafeOutput($Name) . ''; } else { - echo ''.$Name.''; + echo '' . SafeOutput($Name) . ''; } echo ' '.date("d.m.Y H:i", $Reflector->Peers[$i]->GetLastHeardTime()).' '.FormatSeconds(time()-$Reflector->Peers[$i]->GetConnectTime()).' s - '.$Reflector->Peers[$i]->GetProtocol().' - '.$Reflector->Peers[$i]->GetLinkedModule().''; + '.SafeOutput($Reflector->Peers[$i]->GetProtocol()).' + '.SafeOutput($Reflector->Peers[$i]->GetLinkedModule()).''; if ($PageOptions['PeerPage']['IPModus'] != 'HideIP') { echo ''; $Bytes = explode(".", $Reflector->Peers[$i]->GetIP()); @@ -70,7 +70,7 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) { case 'ShowLast1ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[3]; break; case 'ShowLast2ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[2].'.'.$Bytes[3]; break; case 'ShowLast3ByteOfIP' : echo $PageOptions['PeerPage']['MasqueradeCharacter'].'.'.$Bytes[1].'.'.$Bytes[2].'.'.$Bytes[3]; break; - default : echo ''.$Reflector->Peers[$i]->GetIP().''; + default : echo ''.SafeOutput($Reflector->Peers[$i]->GetIP()).''; } } echo ''; diff --git a/dashboard2/pgs/reflectors.php b/dashboard2/pgs/reflectors.php index f8e1adb..c67214a 100644 --- a/dashboard2/pgs/reflectors.php +++ b/dashboard2/pgs/reflectors.php @@ -38,10 +38,10 @@ for ($i=0;$i '.($i+1).' - '.$NAME.' - '.$COUNTRY.' + ' . SafeOutput($NAME) . ' + ' . SafeOutput($COUNTRY) . ' - '.$COMMENT.' + ' . SafeOutput($COMMENT) . ' '; } diff --git a/dashboard2/pgs/repeaters.php b/dashboard2/pgs/repeaters.php index a7562b9..61dd321 100644 --- a/dashboard2/pgs/repeaters.php +++ b/dashboard2/pgs/repeaters.php @@ -30,10 +30,10 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) { echo ''.$Name.''.$Name.''; } echo ' - Nodes[$i]->GetSuffix(); - echo '" class="pl" target="_blank">'.$Reflector->Nodes[$i]->GetCallSign(); - if ($Reflector->Nodes[$i]->GetSuffix() != "") { echo '-'.$Reflector->Nodes[$i]->GetSuffix(); } + Nodes[$i]->GetSuffix()); + echo '" class="pl" target="_blank">'.SafeOutput($Reflector->Nodes[$i]->GetCallSign()); + if ($Reflector->Nodes[$i]->GetSuffix() != "") { echo '-'.SafeOutput($Reflector->Nodes[$i]->GetSuffix()); } echo ' '; if (($Reflector->Nodes[$i]->GetPrefix() == 'REF') || ($Reflector->Nodes[$i]->GetPrefix() == 'XRF')) { @@ -55,8 +55,8 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) { echo ' '.date("d.m.Y H:i", $Reflector->Nodes[$i]->GetLastHeardTime()).' '.FormatSeconds(time()-$Reflector->Nodes[$i]->GetConnectTime()).' s - '.$Reflector->Nodes[$i]->GetProtocol().' - '.$Reflector->Nodes[$i]->GetLinkedModule().''; + '.SafeOutput($Reflector->Nodes[$i]->GetProtocol()).' + '.SafeOutput($Reflector->Nodes[$i]->GetLinkedModule()).''; if ($PageOptions['RepeatersPage']['IPModus'] != 'HideIP') { echo ' '; diff --git a/dashboard2/pgs/users.php b/dashboard2/pgs/users.php index bc8f92f..5ace5b2 100644 --- a/dashboard2/pgs/users.php +++ b/dashboard2/pgs/users.php @@ -9,6 +9,11 @@ if (!isset($_SESSION['FilterModule'])) { } if (isset($_POST['do'])) { + // Validate CSRF token + if (!isset($_POST['csrf_token']) || !ValidateCSRFToken($_POST['csrf_token'])) { + die('CSRF token validation failed'); + } + if ($_POST['do'] == 'SetFilter') { if (isset($_POST['txtSetCallsignFilter'])) { @@ -17,12 +22,17 @@ if (isset($_POST['do'])) { $_SESSION['FilterCallSign'] = null; } else { - $_SESSION['FilterCallSign'] = $_POST['txtSetCallsignFilter']; - if (strpos($_SESSION['FilterCallSign'], "*") === false) { - $_SESSION['FilterCallSign'] = "*".$_SESSION['FilterCallSign']."*"; + // Validate callsign format (alphanumeric, dash, asterisk only) + if (preg_match('/^[A-Z0-9\-\*]+$/i', $_POST['txtSetCallsignFilter'])) { + $_SESSION['FilterCallSign'] = $_POST['txtSetCallsignFilter']; + if (strpos($_SESSION['FilterCallSign'], "*") === false) { + $_SESSION['FilterCallSign'] = "*".$_SESSION['FilterCallSign']."*"; + } + } else { + // Invalid format, reject it + $_SESSION['FilterCallSign'] = null; } } - } if (isset($_POST['txtSetModuleFilter'])) { @@ -31,9 +41,14 @@ if (isset($_POST['do'])) { $_SESSION['FilterModule'] = null; } else { - $_SESSION['FilterModule'] = $_POST['txtSetModuleFilter']; + // Validate module is single letter A-Z + if (preg_match('/^[A-Z]$/i', $_POST['txtSetModuleFilter'])) { + $_SESSION['FilterModule'] = strtoupper($_POST['txtSetModuleFilter']); + } else { + // Invalid format, reject it + $_SESSION['FilterModule'] = null; + } } - } } } @@ -60,7 +75,8 @@ if ($PageOptions['UserPage']['ShowFilter']) {
- + +
'; @@ -72,7 +88,8 @@ if ($PageOptions['UserPage']['ShowFilter']) {
- + +
@@ -133,16 +150,16 @@ for ($i=0;$i<$Reflector->StationCount();$i++) { echo '' . $Name . '' . $Name . ''; } echo ' - ' . $Reflector->Stations[$i]->GetCallsignOnly() . ' - ' . $Reflector->Stations[$i]->GetSuffix() . ' - - ' . $Reflector->Stations[$i]->GetVia(); + ' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . ' + ' . SafeOutput($Reflector->Stations[$i]->GetSuffix()) . ' + + ' . SafeOutput($Reflector->Stations[$i]->GetVia()); if ($Reflector->Stations[$i]->GetPeer() != $Reflector->GetReflectorName()) { - echo ' / ' . $Reflector->Stations[$i]->GetPeer(); + echo ' / ' . SafeOutput($Reflector->Stations[$i]->GetPeer()); } echo ' ' . @date("d.m.Y H:i", $Reflector->Stations[$i]->GetLastHeardTime()) . ' - ' . $Reflector->Stations[$i]->GetModule() . ' + ' . SafeOutput($Reflector->Stations[$i]->GetModule()) . ' '; } if ($i == $PageOptions['LastHeardPage']['LimitTo']) { @@ -192,7 +209,7 @@ for ($i=0;$iGetCallsignAndSuffixByID($Users[$j]); echo ' - '.$Displayname.' + ' . SafeOutput($Displayname) . ' '; $UserCheckedArray[] = $Users[$j]; }