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.
";
+ // 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 "
";
}
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 "; ?>