Compare commits

...

6 commits

Author SHA1 Message Date
Diman Todorov 4727ef2244
Merge 48f0d867ad into 32c3241de0 2025-10-14 08:54:36 -07:00
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
Diman Todorov 48f0d867ad
Addressed PR comment
Removed last byte of disconnect packet to ensure equivalence with ircdbgw.
2022-09-21 23:47:55 -07:00
Diman Todorov c220fa2b61
Added missing 0x20 in DCS EncodeDisconnectPacket
As is, EncodeDisconnectPacket will fail IsValidDisconnectPacket check. The encoded packet is only has 18 bytes instead of 19 because it's missing a 0x20.
2022-09-03 23:33:30 -07:00
12 changed files with 129 additions and 47 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];
}

View file

@ -526,10 +526,10 @@ void CDcsProtocol::EncodeDisconnectPacket(CBuffer *Buffer, CClient *Client)
Buffer->Set((uint8 *)(const char *)Client->GetCallsign(), CALLSIGN_LEN-1);
Buffer->Append((uint8)' ');
Buffer->Append((uint8)Client->GetModule());
Buffer->Append((uint8)' ');
Buffer->Append((uint8)0x00);
Buffer->Append((uint8 *)(const char *)GetReflectorCallsign(), CALLSIGN_LEN-1);
Buffer->Append((uint8)' ');
Buffer->Append((uint8)0x00);
}
void CDcsProtocol::EncodeDvPacket(const CDvHeaderPacket &Header, const CDvFramePacket &DvFrame, uint32 iSeq, CBuffer *Buffer) const