Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b20e41b
Security: Fix path traversal, remove double URL-decoding, add CSRF va…
gehrleib Feb 12, 2026
6c9bc6b
Bugs: Fix undefined function, duplicate code block, and inefficient I/O
gehrleib Feb 12, 2026
be3a730
Code quality: Centralise duplicated file path constants
gehrleib Feb 12, 2026
6803c64
Shell scripts: Remove unnecessary eval, add error handling, quote var…
gehrleib Feb 12, 2026
68944e2
Front-end: Fix XSS in name/description editing and tooltipster
gehrleib Feb 12, 2026
0a9e4c3
Security: Harden input validation, path traversal, CSRF, and config i…
gehrleib Feb 12, 2026
cf011f1
Bugs: Batch I/O in checkAllStacksUpdates, fix double-echo, optimize c…
gehrleib Feb 12, 2026
fd99349
Code quality: Deduplicate sanitization, centralise constants, use unl…
gehrleib Feb 12, 2026
dc7c160
Shell: Remove all eval usage, use arrays for safe argument handling
gehrleib Feb 12, 2026
7b44724
Front-end: Fix XSS in deleteStack dialog, descriptions, and editor fi…
gehrleib Feb 12, 2026
f237641
Config: Use tempnam() and escapeshellarg() for crontab temp files
gehrleib Feb 12, 2026
c8fa00d
Round 3: Shell injection hardening, XSS fixes, input sanitization
gehrleib Feb 12, 2026
a34605b
Round 4: CSRF fix, buildComposeArgs helper, eval removal, dead code c…
gehrleib Feb 12, 2026
e125cc3
Fix ComposeUtilTest: remove incorrect urlencode() from POST data
gehrleib Feb 12, 2026
4aedc6d
Remove CSRF validation — plugin runs behind Unraid auth
gehrleib Feb 12, 2026
9acd38f
fix: source column header color and stack expand flash
gehrleib Feb 12, 2026
ddc52f5
fix: create /mnt/ directory in testSetEnvPathCreatesFile for CI
gehrleib Feb 12, 2026
0b21cb3
fix: skip testSetEnvPathCreatesFile when /mnt/ is not writable
gehrleib Feb 12, 2026
4ee9c81
fix: allow compose_root as valid env path, make test portable
gehrleib Feb 12, 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
62 changes: 52 additions & 10 deletions source/compose.manager/compose.manager.dashboard.page
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ EOT;
// Debug setting from config
$debugEnabled = isset($cfg['DEBUG_TO_LOG']) && $cfg['DEBUG_TO_LOG'] === 'true' ? 'true' : 'false';
$hideComposeContainersJs = $hideDockerComposeContainers ? 'true' : 'false';

// CSS and JavaScript
$configScript = <<<EOT
<script>
Expand Down Expand Up @@ -166,6 +165,25 @@ $script = <<<'EOT'
}
}

// HTML escape function to prevent XSS
function escapeHtml(text) {
if (text === null || text === undefined) return '';
var div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}

// Escape for HTML attributes (more strict)
function escapeAttr(text) {
if (text === null || text === undefined) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}

// Resolve WebUI URL placeholders for a container
// As of this version, WebUI URLs are resolved server-side in exec.php
// (matching Unraid's DockerClient logic for IP/PORT resolution).
Expand Down Expand Up @@ -446,10 +464,9 @@ $script = <<<'EOT'
var stateText = isRunning ? 'started' : 'stopped';
var ctElId = 'dash-ct-' + ct.ID.substring(0, 12);
var imgSrc = ct.Icon || '/plugins/dynamix.docker.manager/images/question.png';
var escapedName = ct.Name.replace(/'/g, "\\'");
var webui = resolveContainerWebUI(ct.WebUI).replace(/'/g, "\\'");
var shell = (ct.Shell || '/bin/bash').replace(/'/g, "\\'");
var shell = ct.Shell || '/bin/bash';
var shortId = ct.ID.substring(0, 12);
var webui = resolveContainerWebUI(ct.WebUI);
// Parse image to get repo and tag
var image = ct.Image || '';
var imageDisplay = image.replace(/^.*\//, ''); // Remove registry prefix
Expand All @@ -471,8 +488,8 @@ $script = <<<'EOT'
var ctUptime = formatUptime(ct.StartedAt, isRunning);

html += '<div class="compose-dash-container">';
html += '<span class="compose-dash-ct-icon" id="' + ctElId + '" onclick="addContainerContext(\'' + ctElId + '\',\'' + escapedName + '\',\'' + shortId + '\',' + isRunning + ',\'' + webui + '\',\'' + shell + '\')">';
html += '<img src="' + imgSrc + '" onerror="this.src=\'/plugins/dynamix.docker.manager/images/question.png\';">';
html += '<span class="compose-dash-ct-icon" id="' + ctElId + '" data-ct-name="' + ct.Name + '" data-ct-id="' + shortId + '" data-ct-running="' + (isRunning ? '1' : '0') + '" data-ct-webui="' + escapeAttr(webui) + '" data-ct-shell="' + escapeAttr(shell) + '">';
html += '<img src="' + escapeAttr(imgSrc) + '" onerror="this.src=\'/plugins/dynamix.docker.manager/images/question.png\';">';
html += '</span>';
html += '<span class="compose-dash-ct-info">';
html += '<div class="compose-dash-ct-name">' + ct.Name + '</div>';
Expand Down Expand Up @@ -540,14 +557,13 @@ $script = <<<'EOT'
var uptimeText = formatUptime(stack.startedAt, isRunning);

// WebUI
var webui = (stack.webui || '').replace(/'/g, "\\'");
var webui = (stack.webui || '');

var escapedFolder = stack.folder.replace(/'/g, "\\'");
// Add state class for filtering (like Docker tile)
var stateClass = state === 'stopped' ? 'stopped' : 'started';
html += '<div class="compose-dash-stack outer stacks ' + stateClass + '" data-stackid="' + stackId + '" data-folder="' + escapedFolder + '">';
html += '<div class="compose-dash-stack outer stacks ' + stateClass + '" data-stackid="' + stackId + '" data-folder="' + stack.folder + '">';
html += '<i class="fa fa-chevron-right compose-dash-expand" id="compose-dash-exp-' + stackId + '"></i>';
html += '<span class="compose-dash-icon" id="compose-dash-icon-' + stackId + '" onclick="addStackContext(\'compose-dash-icon-' + stackId + '\',\'' + escapedFolder + '\',' + isRunning + ',\'' + webui + '\')"><img src="' + imgSrc + '" onerror="this.src=\'/plugins/dynamix.docker.manager/images/question.png\';"></span>';
html += '<span class="compose-dash-icon" id="compose-dash-icon-' + stackId + '" data-stack-folder="' + stack.folder + '" data-stack-running="' + (isRunning ? '1' : '0') + '" data-stack-webui="' + escapeAttr(webui) + '"><img src="' + escapeAttr(imgSrc) + '" onerror="this.src=\'/plugins/dynamix.docker.manager/images/question.png\';"></span>';
html += '<span class="compose-dash-info">';
html += '<div class="compose-dash-name">' + $('<div>').text(stack.name).html() + '</div>';
html += '<div class="compose-dash-state ' + stateColor + '"><i class="fa ' + stateIcon + '"></i> ' + stateText + '</div>';
Expand Down Expand Up @@ -628,6 +644,32 @@ $script = <<<'EOT'
window.composeToggleStack = toggleStack;
window.addStackContext = addStackContext;

// Event delegation for container icon clicks (replaces inline onclick)
$(document).on('click', '.compose-dash-ct-icon[data-ct-name]', function(e) {
e.stopPropagation();
var $el = $(this);
addContainerContext(
$el.attr('id'),
$el.data('ct-name'),
$el.data('ct-id'),
$el.data('ct-running') === '1' || $el.data('ct-running') === 1,
$el.data('ct-webui') || '',
$el.data('ct-shell') || '/bin/bash'
);
});

// Event delegation for stack icon clicks (replaces inline onclick)
$(document).on('click', '.compose-dash-icon[data-stack-folder]', function(e) {
e.stopPropagation();
var $el = $(this);
addStackContext(
$el.attr('id'),
$el.data('stack-folder'),
$el.data('stack-running') === '1' || $el.data('stack-running') === 1,
$el.data('stack-webui') || ''
);
});

// Check if any stacks are visible (for "no stacks" message)
function noStacks() {
if ($('#compose_dash_content .compose-dash-stack:visible').length === 0 && $('#compose_dash_content .compose-dash-stack').length > 0) {
Expand Down
Loading