Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
00a8773
fix missing files.
SimonFair Dec 8, 2025
d86781f
Backport ReadMe not populating in templates
Squidly271 Dec 12, 2025
0c06496
backport: fix: disable rc.avahidnsconfd by default
ljm42 Dec 12, 2025
49b4844
backport: feat: add lsusb details to diagnostics
ljm42 Dec 12, 2025
a26899d
Merge pull request #2476 from SimonFair/Fix-missing-novnc-files
limetech Dec 17, 2025
8f978b9
Merge pull request #2481 from Squidly271/patch-3
limetech Dec 17, 2025
e6db476
Merge pull request #2482 from unraid/backport-fix-disable-avahidnsconfd
limetech Dec 17, 2025
67b4025
Merge pull request #2483 from unraid/backport-add-usb-details-to-diag…
limetech Dec 17, 2025
e90c700
move redirect.htm from webgui repo to api repo
limetech Dec 17, 2025
e8b0f56
fix: Possible XSS via email test functionality
limetech Dec 17, 2025
b0e59c6
fix: persist rclone config via rc.local with hardened init script
elibosley Feb 21, 2026
5d30937
fix: address rclone_config_init backup and nitpick issues
elibosley Feb 21, 2026
976dc1a
Merge pull request #2550 from unraid/fix/persist-rclone-configs-7.2
limetech Feb 23, 2026
aeb2ac2
fix for peer address.
SimonFair Feb 23, 2026
6714f1c
Merge pull request #2555 from SimonFair/fix(WG)-passing-ip-with-peer
limetech Feb 23, 2026
c32847e
Fix rc.sshd: auto-restart SSH daemon after network recovery
SimonFair Feb 23, 2026
5dca79a
Merge pull request #2556 from SimonFair/fix(WG)-passing-ip-with-peer
limetech Feb 23, 2026
af6c3ec
Revert "fix: persist rclone config via rc.local with hardened init sc…
elibosley Feb 24, 2026
2ac392d
Merge pull request #2557 from unraid/revert-2550-fix/persist-rclone-c…
elibosley Feb 24, 2026
77554d8
fix(docker): filter ghost containers from WebGUI
elibosley Apr 12, 2026
cfc4734
fix(mover): allow empty action without pools
SimonFair Feb 20, 2026
fdebbb2
fix(tailscale): reset stale serve state on restart
elibosley Mar 31, 2026
254ed23
Merge pull request #2611 from unraid/codex/7.2-filter-docker-ghost-co…
limetech Apr 14, 2026
9294349
Merge pull request #2615 from unraid/codex/7.2-mover-empty-no-pool
limetech Apr 14, 2026
1f37e1e
Merge pull request #2616 from unraid/codex/7.2-tailscale-funnel-reset
limetech Apr 14, 2026
69b29ab
Ignore redirect.htm now provided by Unraid API
limetech Feb 23, 2026
ff1bd54
fix: case image display
limetech Apr 14, 2026
cd1d75a
fix(docker): restore custom networks reliably on 7.2
limetech Mar 24, 2026
90d05d0
fix(docker): assign fixed MACs through network endpoints
limetech Apr 22, 2026
51e1124
fix(docker): show container MAC addresses on 7.2
limetech Apr 22, 2026
ec18cf9
Merge pull request #2623 from unraid/codex/7.2-docker-mac-assignment
limetech Apr 24, 2026
2927fb0
Merge pull request #2624 from unraid/codex/7.2-docker-mac-display
limetech Apr 24, 2026
a6fd094
Merge remote-tracking branch 'upstream/master' into 7.2
SimonFair Apr 28, 2026
aaf524f
fix: misc. security mitigations
limetech May 12, 2026
da7ba5d
fix(docker): use configured gateways for VLAN networks
elibosley May 11, 2026
ea15629
fix: normalize discord agent newlines
pujitm Jan 21, 2026
1fb0869
Fix dropdown z-index
Joly0 May 14, 2026
19db0e3
Update dropdown spacer element in docker.js
Joly0 May 14, 2026
1cbad60
Merge pull request #2637 from unraid/codex/7.2-docker-vlan-gateway-fa…
limetech May 14, 2026
f4dfa05
Merge pull request #2638 from unraid/codex/7.2-discord-newlines
limetech May 14, 2026
135c521
Merge pull request #2640 from unraid/codex/backport-joly0-docker-drop…
limetech May 14, 2026
de7bf2f
Move unRAIDSserver plugin to webGUI repo.
limetech May 14, 2026
a21050c
Merge branch '7.2' of https://github.com/unraid/webgui into 7.2
SimonFair May 28, 2026
6b01726
Update monitor
SimonFair May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions emhttp/auth-request.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,52 @@
session_write_close();
}

// Include JS caching functions
require_once '/usr/local/emhttp/webGui/include/JSCache.php';
function isPathInDocroot(string $realPath, string $docroot): bool {
return $realPath === $docroot || str_starts_with($realPath, $docroot . '/');
}

function getCanonicalRequestUri(string $docroot): string {
$requestUri = getRequestUriPath();

$realRequestPath = realpath($docroot . '/' . ltrim($requestUri, '/'));
if (!is_string($realRequestPath) || !isPathInDocroot($realRequestPath, $docroot)) {
return '';
}

$canonicalRequestUri = substr($realRequestPath, strlen($docroot));
return $canonicalRequestUri === '' ? '/' : $canonicalRequestUri;
}

function isWebComponentsRequest(string $requestUri): bool {
$webComponentsDirectory = '/plugins/dynamix.my.servers/unraid-components';
return $requestUri === $webComponentsDirectory || str_starts_with($requestUri, $webComponentsDirectory . '/');
}

function getRequestUriPath(): string {
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
return is_string($requestUri) ? $requestUri : '/';
}

function getAllowedExternalPublicAssetTargets(): array {
return [
'/webGui/images/case-model.png' => '/boot/config/plugins/dynamix/case-model.png',
];
}

function isAllowedPublicAssetRequest(string $requestUri, string $docroot, array $arrWhitelist): bool {
if (!in_array($requestUri, $arrWhitelist, true)) {
return false;
}

$realRequestPath = realpath($docroot . '/' . ltrim($requestUri, '/'));
if (is_string($realRequestPath) && isPathInDocroot($realRequestPath, $docroot)) {
return true;
}

$allowedExternalTargets = getAllowedExternalPublicAssetTargets();
return isset($allowedExternalTargets[$requestUri]) &&
$realRequestPath === $allowedExternalTargets[$requestUri];
}

// Base whitelist of files
$arrWhitelist = [
Expand Down Expand Up @@ -54,12 +98,22 @@
'/manifest.json'
];

// Whitelist ALL files from the unraid-components directory
$webComponentsDirectory = '/plugins/dynamix.my.servers/unraid-components/';
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? '/';
// Use canonical filesystem path checks against the trusted docroot.
$docroot = '/usr/local/emhttp';
$requestUri = getRequestUriPath();
$canonicalRequestUri = getCanonicalRequestUri($docroot);

// Allow explicit public assets with strict target checks.
if (isAllowedPublicAssetRequest($requestUri, $docroot, $arrWhitelist)) {
http_response_code(200);
exit;
}

// Check if the request is for any file in the unraid-components directory
if (str_starts_with($requestUri, $webComponentsDirectory) || in_array($requestUri, $arrWhitelist)) {
// Allow canonical requests under unraid-components.
if (
$canonicalRequestUri !== '' &&
isWebComponentsRequest($canonicalRequestUri)
) {
// authorized
http_response_code(200);
} else {
Expand Down
2 changes: 2 additions & 0 deletions emhttp/plugins/dynamix.docker.manager/javascript/docker.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ function addDockerContainerContext(container, image, template, started, paused,
}
context.destroy('#'+id);
context.attach('#'+id, opts);
$('#dropdown-'+id).css('z-index', 10001)
.append('<li class="docker-dropdown-spacer" aria-hidden="true" style="position:absolute;top:100%;left:0;width:1px;height:60px;pointer-events:none;list-style:none"></li>');
}
function addDockerImageContext(image, imageTag) {
var opts = [];
Expand Down
70 changes: 63 additions & 7 deletions emhttp/plugins/dynamix.plugin.manager/scripts/language
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ function run($command) {
return pclose($run);
}

function remove_tree(string $dir): bool {
if (!is_dir($dir)) return false;
$items = scandir($dir);
if ($items === false) return false;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$entry = "$dir/$item";
if (is_dir($entry) && !is_link($entry)) {
if (!remove_tree($entry)) return false;
} elseif (!@unlink($entry)) {
return false;
}
}
return @rmdir($dir);
}

function valid_language_pack_name($name): bool {
return is_string($name) && $name !== '' && preg_match('/^[A-Za-z0-9._$-]+$/', $name);
}

// Run hooked scripts before correct execution of "method"
// method = install, update, remove, check
// hook programs receives three parameters: type=language and method and language-name
Expand Down Expand Up @@ -159,7 +179,11 @@ function language($method, $xml_file, &$error) {
switch ($method) {
case 'install':
$url = $xml->LanguageURL;
$name = $xml->LanguagePack;
$name = (string)$xml->LanguagePack;
if (!valid_language_pack_name($name)) {
$error = "invalid language pack";
return false;
}
$save = "$boot/dynamix/lang-$name.zip";
if (!file_exists($save)) {
if ($url) {
Expand All @@ -173,22 +197,54 @@ function language($method, $xml_file, &$error) {
}
}
$path = "$docroot/languages/$name";
exec("mkdir -p $path");
if (!is_dir($path) && !@mkdir($path, 0777, true)) {
$error = "failed to create language directory";
return false;
}
@unlink("$docroot/webGui/javascript/translate.$name.js");
foreach (glob("$path/*.dot",GLOB_NOSORT) as $dot_file) unlink($dot_file);
exec("unzip -qqLjo -d $path $save", $dummy, $err);
foreach (['*.dot', '*.txt', '*.json'] as $pattern) {
foreach (glob("$path/$pattern", GLOB_NOSORT) as $lang_file) @unlink($lang_file);
}
$err = 0;
$zip = new ZipArchive();
if ($zip->open($save) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);
if ($entry === false) continue;
if (substr($entry, -1) === '/') continue;
$content = $zip->getFromIndex($i);
if ($content === false) {
$err = 2;
break;
}
$entryName = strtolower(basename($entry));
if ($entryName === '') continue;
if (!preg_match('/\.(dot|txt|json)$/', $entryName)) continue;
if (file_put_contents("$path/$entryName", $content) === false) {
$err = 2;
break;
}
}
$zip->close();
} else {
$err = 2;
}
if ($err > 1) {
@unlink($save);
exec("rm -rf $path");
remove_tree($path);
$error = "unzip failed. Error code $err";
return false;
}
return true;
case 'remove':
$name = $xml->LanguagePack;
$name = (string)$xml->LanguagePack;
if ($name) {
if (!valid_language_pack_name($name)) {
$error = "invalid language pack";
return false;
}
$path = "$docroot/languages/$name";
exec("rm -rf $path");
if (is_dir($path)) remove_tree($path);
@unlink("$docroot/webGui/javascript/translate.$name.js");
@unlink("$boot/lang-$name.xml");
@unlink("$plugins/lang-$name.xml");
Expand Down
7 changes: 6 additions & 1 deletion emhttp/plugins/dynamix.vm.manager/VMMachines.page
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,12 @@ function tableHeaderResize() {
$(function() {
<?if ($msg):?>
<?$color = strpos($msg, "rror:")!==false ? 'red-text':'green-text'?>
$('#countdown').html("<span class='<?=$color?>'><?=_($msg)?></span>");
$('#countdown').empty().append(
$('<span/>', {
class: '<?=$color?>',
text: <?=json_encode(_($msg), JSON_HEX_TAG|JSON_HEX_AMP|JSON_HEX_APOS|JSON_HEX_QUOT)?>
})
);
<?endif;?>
$('#btnAddVM').click(function AddVMEvent(){$('.tab>input#tab2').click();});
$.removeCookie('lockbutton');
Expand Down
84 changes: 73 additions & 11 deletions emhttp/plugins/dynamix/include/FileUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@
$safeexts = ['.png'];
$result = false;

function in_safe_path(string $path, string $base): bool {
$path = rtrim($path, '/').'/';
$base = rtrim($base, '/').'/';
return strpos($path, $base) === 0;
}

function remove_tree(string $dir): bool {
if (!is_dir($dir)) return false;
$items = scandir($dir);
if ($items === false) return false;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$entry = "$dir/$item";
if (is_dir($entry) && !is_link($entry)) {
if (!remove_tree($entry)) return false;
} elseif (!@unlink($entry)) {
return false;
}
}
return @rmdir($dir);
}

require_once "$docroot/webGui/include/Helpers.php";

switch ($_POST['cmd'] ?? 'load') {
Expand All @@ -47,20 +69,37 @@
case 'save':
// move uploaded file ($verifiedPNG) to final destination
$verifiedPNG = "$temp/".basename($file);
$path = $_POST['path'];
$path = $_POST['path'] ?? '';
$outputRaw = $_POST['output'] ?? '';
$output = basename($outputRaw);
$outputExt = strtolower(substr($output, -4));
$isValidFilename = $output !== '' && $output === $outputRaw && preg_match('/^[A-Za-z0-9._$-]+$/', $output);
foreach ($safepaths as $safepath) {
if (strpos(dirname("$path/{$_POST['output']}"),$safepath)===0 && in_array(substr(basename($_POST['output']),-4),$safeexts)) {
exec("mkdir -p ".escapeshellarg(realpath($path)));
$result = @rename($verifiedPNG, "$path/{$_POST['output']}");
$safeBase = realpath($safepath);
$targetDir = realpath($path);
if (!$targetDir && $safeBase) {
$parentDir = realpath(dirname($path));
if ($parentDir && in_safe_path($parentDir, $safeBase) && @mkdir($path, 0777, true)) {
$targetDir = realpath($path);
}
}
if ($targetDir && $safeBase && in_safe_path($targetDir, $safeBase) && $isValidFilename && in_array($outputExt, $safeexts, true)) {
if (is_dir($targetDir)) {
$result = @rename($verifiedPNG, "$targetDir/$output");
}
break;
}
}
break;
case 'delete':
$path = $_POST['path'];
$path = $_POST['path'] ?? '';
$file = basename($file);
$targetFile = realpath("$path/$file");
$targetExt = $targetFile ? strtolower(substr($targetFile, -4)) : '';
foreach ($safepaths as $safepath) {
if (strpos(realpath("$path/$file"), $safepath) === 0 && in_array(substr(realpath("$path/$file"), -4), $safeexts)) {
exec("rm -f ".escapeshellarg(realpath("$path/$file")));
$safeBase = realpath($safepath);
if ($targetFile && $safeBase && in_safe_path($targetFile, $safeBase) && in_array($targetExt, $safeexts, true)) {
@unlink($targetFile);
$result = true;
break;
}
Expand All @@ -70,14 +109,37 @@
$file = basename($file);
$path = "$docroot/languages/$file";
$save = "/tmp/lang-$file.zip";
exec("mkdir -p $path");
if (!is_dir($path) && !@mkdir($path, 0777, true)) break;
if ($result = file_put_contents($save,base64_decode(preg_replace('/^data:.*;base64,/','',$_POST['filedata'])))) {
@unlink("$docroot/webGui/javascript/translate.$file.js");
foreach (glob("$path/*.dot",GLOB_NOSORT) as $dot_file) unlink($dot_file);
exec("unzip -qqjLo -d $path $save", $dummy, $err);
$err = 0;
$zip = new ZipArchive();
if ($zip->open($save) === true) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);
if ($entry === false) continue;
if (substr($entry, -1) === '/') continue;
$content = $zip->getFromIndex($i);
if ($content === false) {
$err = 2;
break;
}
$name = strtolower(basename($entry));
if ($name === '') continue;
if (!preg_match('/\.(dot|txt|json)$/', $name)) continue;
if (file_put_contents("$path/$name", $content) === false) {
$err = 2;
break;
}
}
$zip->close();
} else {
$err = 2;
}
@unlink($save);
if ($err > 1) {
exec("rm -rf $path");
remove_tree($path);
$result = false;
break;
}
Expand All @@ -101,7 +163,7 @@
$file = basename($file);
$path = "$docroot/languages/$file";
if ($result = is_dir($path)) {
exec("rm -rf $path");
$result = remove_tree($path);
@unlink("$docroot/webGui/javascript/translate.$file.js");
@unlink("$boot/lang-$file.xml");
@unlink("$plugins/lang-$file.xml");
Expand Down
2 changes: 1 addition & 1 deletion emhttp/plugins/dynamix/include/ToggleState.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
$action = $_POST['action']??'';

function emcmd($cmd) {
exec("emcmd '$cmd'");
exec("emcmd ".escapeshellarg($cmd));
}
switch ($device) {
case 'New':
Expand Down
Loading
Loading