Compare commits

...

3 commits

Author SHA1 Message Date
LX1IQ 32c3241de0
Merge pull request #253 from MW0MWZ/master
Dashboard2 XSS / Security fixes
2025-10-14 13:43:36 +02:00
Andy Taylor 80821c25a3 Remove .DS_Store and update .gitignore 2025-10-14 12:26:32 +01:00
Andy Taylor 61204c3ed4 XSS Vulnerability Patches and Security Enhancements for Dashboard2 2025-10-14 12:25:26 +01:00
11 changed files with 128 additions and 46 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
src/xlxd
ambed/ambed
ambedtest/ambedtest
.DS_Store

View file

@ -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 servers /tmp folder.
On the first run your personal hash to access the database is place in the server<EFBFBD>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 <EFBFBD>land Islands
xlx db v2.1.3

View file

@ -1,4 +1,5 @@
<?php
session_start();
/*
* This dashboard is being developed by the DVBrazil Team as a courtesy to
@ -9,12 +10,12 @@
if (file_exists("./pgs/functions.php")) {
require_once("./pgs/functions.php");
} else {
die("functions.php does not exist.");
die("Required file not found.");
}
if (file_exists("./pgs/config.inc.php")) {
require_once("./pgs/config.inc.php");
} else {
die("config.inc.php does not exist.");
die("Required file not found.");
}
if (!class_exists('ParseXML')) require_once("./pgs/class.parsexml.php");
@ -44,7 +45,7 @@ if ($CallingHome['Active']) {
@fwrite($Ressource, "\n" . '$Hash = "' . $Hash . '";');
@fwrite($Ressource, "\n\n" . '?>');
@fclose($Ressource);
@exec("chmod 777 " . $CallingHome['HashFile']);
@exec("chmod 600 " . $CallingHome['HashFile']);
$CallHomeNow = true;
}
} else {
@ -79,12 +80,11 @@ if ($CallingHome['Active']) {
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="<?php echo $PageOptions['MetaDescription']; ?>"/>
<meta name="keywords" content="<?php echo $PageOptions['MetaKeywords']; ?>"/>
<meta name="author" content="<?php echo $PageOptions['MetaAuthor']; ?>"/>
<meta name="revisit" content="<?php echo $PageOptions['MetaRevisit']; ?>"/>
<meta name="robots" content="<?php echo $PageOptions['MetaAuthor']; ?>"/>
<meta name="description" content="<?php echo SafeOutputAttr($PageOptions['MetaDescription']); ?>"/>
<meta name="keywords" content="<?php echo SafeOutputAttr($PageOptions['MetaKeywords']); ?>"/>
<meta name="author" content="<?php echo SafeOutputAttr($PageOptions['MetaAuthor']); ?>"/>
<meta name="revisit" content="<?php echo SafeOutputAttr($PageOptions['MetaRevisit']); ?>"/>
<meta name="robots" content="<?php echo SafeOutputAttr($PageOptions['MetaAuthor']); ?>"/>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
<title><?php echo $Reflector->GetReflectorName(); ?> Reflector Dashboard</title>
<link rel="icon" href="./favicon.ico" type="image/vnd.microsoft.icon">
@ -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']) {
<footer class="footer">
<div class="container">
<p><a href="mailto:<?php echo $PageOptions['ContactEmail']; ?>"><?php echo $PageOptions['ContactEmail']; ?></a>
<p><a href="mailto:<?php echo SafeOutputAttr($PageOptions['ContactEmail']); ?>"><?php echo SafeOutput($PageOptions['ContactEmail']); ?></a>
</p>
</div>
</footer>

View file

@ -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 = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<query>CallingHome</query>'.$this->ReflectorXML.$this->InterlinkXML;
$p = @stream_context_create(array('http' => array('header' => "Content-type: application/x-www-form-urlencoded\r\n",

View file

@ -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

0
dashboard2/pgs/country.csv Executable file → Normal file
View file

View file

@ -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);
}
?>

View file

@ -53,15 +53,15 @@ for ($i=0;$i<$Reflector->PeerCount();$i++) {
}
}
if ($Result && (trim($URL) != "")) {
echo '<td><a href="'.$URL.'" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;'.$Name.'" style="text-decoration:none;color:#000000;">'.$Name.'</a></td>';
echo '<td><a href="' . SafeOutputAttr($URL) . '" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;' . SafeOutputAttr($Name) . '" style="text-decoration:none;color:#000000;">' . SafeOutput($Name) . '</a></td>';
} else {
echo '<td>'.$Name.'</td>';
echo '<td>' . SafeOutput($Name) . '</td>';
}
echo '
<td>'.date("d.m.Y H:i", $Reflector->Peers[$i]->GetLastHeardTime()).'</td>
<td>'.FormatSeconds(time()-$Reflector->Peers[$i]->GetConnectTime()).' s</td>
<td>'.$Reflector->Peers[$i]->GetProtocol().'</td>
<td>'.$Reflector->Peers[$i]->GetLinkedModule().'</td>';
<td>'.SafeOutput($Reflector->Peers[$i]->GetProtocol()).'</td>
<td>'.SafeOutput($Reflector->Peers[$i]->GetLinkedModule()).'</td>';
if ($PageOptions['PeerPage']['IPModus'] != 'HideIP') {
echo '<td>';
$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 '<a href="http://'.$Reflector->Peers[$i]->GetIP().'" target="_blank" style="text-decoration:none;color:#000000;">'.$Reflector->Peers[$i]->GetIP().'</a>';
default : echo '<a href="http://'.SafeOutput($Reflector->Peers[$i]->GetIP()).'" target="_blank" style="text-decoration:none;color:#000000;">'.SafeOutput($Reflector->Peers[$i]->GetIP()).'</a>';
}
}
echo '</td>';

View file

@ -38,10 +38,10 @@ for ($i=0;$i<count($Reflectors);$i++) {
echo '
<tr class="table-center">
<td>'.($i+1).'</td>
<td><a href="'.$DASHBOARDURL.'" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;'.$NAME.'">'.$NAME.'</a></td>
<td>'.$COUNTRY.'</td>
<td><a href="' . SafeOutputAttr($DASHBOARDURL) . '" target="_blank" class="listinglink" title="Visit the Dashboard of&nbsp;' . SafeOutputAttr($NAME) . '">' . SafeOutput($NAME) . '</a></td>
<td>' . SafeOutput($COUNTRY) . '</td>
<td><img src="./img/'; if ($LASTCONTACT<(time()-1800)) { echo 'down'; } ELSE { echo 'up'; } echo '.png" class="table-status" alt=""></td>
<td>'.$COMMENT.'</td>
<td>' . SafeOutput($COMMENT) . '</td>
</tr>';
}

View file

@ -30,10 +30,10 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) {
echo '<a href="#" class="tip"><img src="./img/flags/'.$Flag.'.png" class="table-flag" alt="'.$Name.'"><span>'.$Name.'</span></a>';
}
echo '</td>
<td><a href="http://www.aprs.fi/'.$Reflector->Nodes[$i]->GetCallSign();
if ($Reflector->Nodes[$i]->GetSuffix() != "") echo '-'.$Reflector->Nodes[$i]->GetSuffix();
echo '" class="pl" target="_blank">'.$Reflector->Nodes[$i]->GetCallSign();
if ($Reflector->Nodes[$i]->GetSuffix() != "") { echo '-'.$Reflector->Nodes[$i]->GetSuffix(); }
<td><a href="http://www.aprs.fi/'.SafeOutput($Reflector->Nodes[$i]->GetCallSign());
if ($Reflector->Nodes[$i]->GetSuffix() != "") echo '-'.SafeOutput($Reflector->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 '</a></td>
<td>';
if (($Reflector->Nodes[$i]->GetPrefix() == 'REF') || ($Reflector->Nodes[$i]->GetPrefix() == 'XRF')) {
@ -55,8 +55,8 @@ for ($i=0;$i<$Reflector->NodeCount();$i++) {
echo '</td>
<td>'.date("d.m.Y H:i", $Reflector->Nodes[$i]->GetLastHeardTime()).'</td>
<td>'.FormatSeconds(time()-$Reflector->Nodes[$i]->GetConnectTime()).' s</td>
<td>'.$Reflector->Nodes[$i]->GetProtocol().'</td>
<td>'.$Reflector->Nodes[$i]->GetLinkedModule().'</td>';
<td>'.SafeOutput($Reflector->Nodes[$i]->GetProtocol()).'</td>
<td>'.SafeOutput($Reflector->Nodes[$i]->GetLinkedModule()).'</td>';
if ($PageOptions['RepeatersPage']['IPModus'] != 'HideIP') {
echo '
<td>';

View file

@ -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 {
// 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']) {
<td align="left">
<form name="frmFilterCallSign" method="post" action="./index.php">
<input type="hidden" name="do" value="SetFilter" />
<input type="text" class="FilterField" value="'.$_SESSION['FilterCallSign'].'" name="txtSetCallsignFilter" placeholder="Callsign" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="hidden" name="csrf_token" value="' . GenerateCSRFToken() . '" />
<input type="text" class="FilterField" value="'.SafeOutputAttr($_SESSION['FilterCallSign']).'" name="txtSetCallsignFilter" placeholder="Callsign" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="submit" value="Apply" class="FilterSubmit" />
</form>
</td>';
@ -72,7 +88,8 @@ if ($PageOptions['UserPage']['ShowFilter']) {
<td align="right" style="padding-right:3px;">
<form name="frmFilterModule" method="post" action="./index.php">
<input type="hidden" name="do" value="SetFilter" />
<input type="text" class="FilterField" value="'.$_SESSION['FilterModule'].'" name="txtSetModuleFilter" placeholder="Module" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="hidden" name="csrf_token" value="' . GenerateCSRFToken() . '" />
<input type="text" class="FilterField" value="'.SafeOutputAttr($_SESSION['FilterModule']).'" name="txtSetModuleFilter" placeholder="Module" onfocus="SuspendPageRefresh();" onblur="setTimeout(ReloadPage, '.$PageOptions['PageRefreshDelay'].');" />
<input type="submit" value="Apply" class="FilterSubmit" />
</form>
</td>
@ -133,16 +150,16 @@ for ($i=0;$i<$Reflector->StationCount();$i++) {
echo '<a href="#" class="tip"><img src="./img/flags/' . $Flag . '.png" class="table-flag" alt="' . $Name . '"><span>' . $Name . '</span></a>';
}
echo '</td>
<td><a href="https://www.qrz.com/db/' . $Reflector->Stations[$i]->GetCallsignOnly() . '" class="pl" target="_blank">' . $Reflector->Stations[$i]->GetCallsignOnly() . '</a></td>
<td>' . $Reflector->Stations[$i]->GetSuffix() . '</td>
<td><a href="http://www.aprs.fi/' . $Reflector->Stations[$i]->GetCallsignOnly() . '" class="pl" target="_blank"><img src="./img/sat.png" alt=""></a></td>
<td>' . $Reflector->Stations[$i]->GetVia();
<td><a href="https://www.qrz.com/db/' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '" class="pl" target="_blank">' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '</a></td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetSuffix()) . '</td>
<td><a href="http://www.aprs.fi/' . SafeOutput($Reflector->Stations[$i]->GetCallsignOnly()) . '" class="pl" target="_blank"><img src="./img/sat.png" alt=""></a></td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetVia());
if ($Reflector->Stations[$i]->GetPeer() != $Reflector->GetReflectorName()) {
echo ' / ' . $Reflector->Stations[$i]->GetPeer();
echo ' / ' . SafeOutput($Reflector->Stations[$i]->GetPeer());
}
echo '</td>
<td>' . @date("d.m.Y H:i", $Reflector->Stations[$i]->GetLastHeardTime()) . '</td>
<td>' . $Reflector->Stations[$i]->GetModule() . '</td>
<td>' . SafeOutput($Reflector->Stations[$i]->GetModule()) . '</td>
</tr>';
}
if ($i == $PageOptions['LastHeardPage']['LimitTo']) {
@ -192,7 +209,7 @@ for ($i=0;$i<count($Modules);$i++) {
$Displayname = $Reflector->GetCallsignAndSuffixByID($Users[$j]);
echo '
<tr>
<td><a href="http://www.aprs.fi/'.$Displayname.'" class="pl" target="_blank">'.$Displayname.'</a> </td>
<td><a href="http://www.aprs.fi/' . SafeOutput($Displayname) . '" class="pl" target="_blank">' . SafeOutput($Displayname) . '</a> </td>
</tr>';
$UserCheckedArray[] = $Users[$j];
}