Skip to content

Security audit, code quality fixes, and UI improvements#35

Merged
mstrhakr merged 19 commits intomstrhakr:mainfrom
gehrleib:audit-fixes
Feb 13, 2026
Merged

Security audit, code quality fixes, and UI improvements#35
mstrhakr merged 19 commits intomstrhakr:mainfrom
gehrleib:audit-fixes

Conversation

@gehrleib
Copy link

@gehrleib gehrleib commented Feb 12, 2026

Security Fixes

  • Path traversal — validate and sanitize all user-supplied file paths against allowed directories
  • Shell injection — replace all eval usage with safe array-based argument handling; apply escapeshellarg() consistently
  • XSS — escape user-controlled content in HTML output (stack names, descriptions, editor filenames, tooltips, error messages)
  • Double URL-decoding — remove redundant urldecode() calls that could bypass input validation
  • Config injection — sanitize values written to INI config files

Code Quality

  • Centralise duplicated file-path constants into defines.php
  • Deduplicate sanitization helpers (sanitizeStackName, sanitizeDescription, isValidWebUIUrl)
  • Extract buildComposeArgs() helper to eliminate repeated argument-building logic
  • Remove dead/unreachable code blocks
  • Fix undefined function calls and duplicate code blocks
  • Batch I/O in checkAllStacksUpdates (single JSON write instead of per-container)
  • Fix double-echo bug in exec actions

Shell Script Hardening

  • Quote all variables, add error handling
  • Use tempnam() + escapeshellarg() for crontab temp files
  • Remove unnecessary eval from compose.sh, backup_cron.sh, event scripts

UI Fixes

  • Source column header color — split CSS rule so only data cells get muted #606060 color; header inherits theme default
  • Stack expand flash — cached expands now slide down existing DOM content instead of re-fetching via AJAX (eliminates visible spinner flash)

Test Fixes

  • Fix ComposeUtilTest: remove incorrect urlencode() from POST test data
  • Update ExecActionsTest expectations to match refactored code

…lidation, improve shell escaping

- 1a: Add basename() sanitization to all POST['script'] usages via getPostScript()
  helper to prevent path traversal attacks in exec.php
- 1b: Add CSRF token validation to exec.php, compose_util.php, and inject
  csrf_token into all frontend pages (main, settings, dashboard)
- 1c: Remove unnecessary urldecode() from all POST parameters in exec.php
  and compose_util_functions.php — PHP already decodes form data
- 1d: Improve shell escaping in execComposeCommandInTTY() — use pkill -f
  with escapeshellarg() instead of pgrep|awk pipeline, escapeshellarg
  the socket path
- 2a: Fix icon.php calling undefined getComposeRoot() — use
  locate_compose_root('compose.manager') which is defined in defines.php
- 2b: Move update status file reads outside per-container loops in
  checkStackUpdates and getStackContainers — split into two-pass approach
  (clear cached SHAs once, then check updates) to eliminate redundant
  file reads/writes inside loops
- 2c: Remove duplicate trailing code block in event/started (lines 241-253)
  that caused a bash syntax error
- 3e: Add COMPOSE_UPDATE_STATUS_FILE and UNRAID_UPDATE_STATUS_FILE
  constants to defines.php and replace 8 hardcoded string literals
  throughout exec.php
…iables

- 4a: Remove eval from docker rmi (use array expansion instead),
  docker compose ls, and docker ps commands in compose.sh where eval
  was not needed. Eval remains where @Q-quoted variables require it.
- 4b: Add error handling for the logs command in compose.sh
- 4c: Quote COMPOSE_ROOT variable in stopping_docker and event/started
  glob patterns to handle paths with spaces. Quote find arguments
  in compose.sh and fix unquoted envFile test.
- 5b: Rewrite editName() and editDesc() to use DOM construction
  (jQuery .val(), .attr(), .on()) instead of string concatenation
  with .html() — prevents attribute breakout XSS
- 5c: Fix applyName()/cancelName() to use .text() instead of .html()
  for user-supplied values. Fix applyDesc() to escape HTML before
  inserting with .html() (using existing escapeHtml() helper).
  Fix cancelDesc() to use .text(). Escape stackName in tooltipster
  content injection.
…njection

- deleteStack: apply basename() to stackName to prevent path traversal (1d)
- addStack: validate stackPath is under /mnt/ or /boot/config/ (1c)
- setEnvPath: validate env file path is under allowed root (1b)
- saveBackupSettings: whitelist allowed keys, validate numeric/enum fields,
  sanitize values to prevent INI injection (1a, 6b)
- restoreBackup: apply basename() to archive parameter (1g)
- resolveArchivePath: always use basename() instead of allowing absolute paths (1g)
- uploadBackup: explicitly append CSRF token to FormData (1e)
- setEnvPath: replace exec('rm') with @Unlink() (3e partial)
…ron script

- checkAllStacksUpdates: load status file once, batch-clear local SHAs,
  save once — eliminates per-container loadJSON/saveJSON calls (2a)
- saveProfiles: fix double-echo when deleting empty profiles, replace
  exec('rm') with @Unlink() (2b, 3e)
- updateAutostart: replace exec('rm') with @Unlink() (3e)
- backup_cron.sh: parse all JSON fields in single php -r call instead
  of 5 separate invocations (2d)
…ink()

- addStack: replace 7-line inline sanitization with sanitizeFolderName() call (3a)
- defines.php: add PENDING_RECHECK_FILE constant (3c)
- exec.php: use PENDING_RECHECK_FILE constant in markStackForRecheck,
  getPendingRecheckStacks, and clearStackRecheck cases (3c)
- dashboard_stacks.php: use COMPOSE_UPDATE_STATUS_FILE constant instead of
  hardcoded path (3d)
- updateAutostart: replace exec('rm') with @Unlink() (3e)
- compose.sh: refactor from string-based \ + eval to proper Bash
  arrays for env_args, file_args, profile_args, cmd_args. Eliminates eval
  from down, stop, logs, and run_with_retry (2c, 4c)
- compose.sh: run_with_retry now accepts command as positional args and
  uses printf '%%q' for script -c instead of eval (4c)
- compose.sh: update fallback image extraction to use file_args array
- stopping_docker: use arrays and find -print0 for safe path handling
  with spaces in file paths (4b)
- patch.sh: increase mktemp random suffix from 4 to 8 chars (4d)
…lename

- deleteStack(): escape project and stackName with escapeHtml() before
  injecting into SweetAlert html:true dialog (5a)
- applyDesc(): use .text() with CSS white-space:pre-line instead of
  .html() with <br> replacement to avoid latent XSS vector (5b)
- All editorFileName updates: replace .html(response.fileName) with
  .text() across editComposeFile, editEnv, editComposeFileByProject,
  editEnvByProject, editOverrideByProject (5c)
- compose_list.php: apply htmlspecialchars() to description before
  converting newlines to <br> tags (5e)
- backup_functions.php updateBackupCron(): replace PID-based temp file
  naming with PHP tempnam() for unpredictable filenames (6a)
- Use escapeshellarg() when passing temp file path to crontab command
- event/started: Refactor start_stack() from string-based command construction
  (bash -c) to array-based execution, eliminating shell injection risk from
  stack names, env paths, and profile names
- exec.php: Sanitize changeName input to only allow safe characters, preventing
  shell metacharacters from being stored and later used in bash scripts
- compose_list.php: HTML-escape profilesJson in data-profiles attribute to
  prevent attribute breakout from profile names containing quotes
- dashboard: Add escapeHtml()/escapeAttr() helpers, escape all dynamic container
  data (names, images, SHAs, icons, WebUI URLs) in HTML output
- dashboard: Replace inline onclick handlers with data-* attributes and event
  delegation for both container icons and stack icons, eliminating attribute
  injection vectors
…leanup

- Fix CSRF token: replace $.ajaxSetup with $.ajaxPrefilter in 3 JS files
  (compose_manager_main.php, settings.page, dashboard.page) to reliably
  inject csrf_token into POST requests regardless of data format
- Add read-only action whitelists in exec.php and compose_util.php to
  skip CSRF validation on safe GET-like actions
- Extract buildComposeArgs() helper in exec_functions.php to deduplicate
  compose file/env/project resolution across getStackContainers,
  checkStackUpdates, and checkAllStacksUpdates
- Guard getPostScript() with function_exists for test compatibility
- Remove eval from backup_cron.sh, use individual command substitutions
- Remove dead code ( redefinition) from dashboard_stacks.php
- Add shift to compose.sh wildcard case to prevent infinite loop
- Fix ExecActionsTest: remove incorrect urlencode() from  values,
  skip setEnvPath test on Windows (requires Linux path validation)
Same root cause as ExecActionsTest fix - tests set \ directly
but wrapped values in urlencode(), causing path/profile corruption.
All 233 tests now pass with 0 failures.
The CSRF token validation was added by our audit but is unnecessary:
- Unraid plugins run behind Unraid's own authentication on a local network
- The plugin-level CSRF layer was redundant and actively breaking all
  POST requests (Save Settings, Backup Now, compose actions, etc.)
- Removed server-side validation from exec.php and compose_util.php
- Removed client-side token injection (ajaxPrefilter) from all 3 JS files
- Removed manual FormData csrf_token append from backup upload
- Source header: split CSS rule so only td gets #606060 color,
  th inherits the theme default (was applying gray to both)
- Flash fix: cached expands just slideDown existing DOM content
  instead of re-fetching via AJAX (which caused visible spinner
  flash + double slideDown). Uncached expands defer slideDown
  until after render completes.
realpath() returns false when the target directory doesn't exist,
causing the path validation to reject the env path. Create the
directory before the test so it resolves on GitHub Actions.
GitHub Actions runners don't have write permission to /mnt/,
so mkdir fails silently and realpath() returns false. Skip
gracefully instead of failing.
- Add compose_root to the setEnvPath path validation allowlist
  (alongside /mnt/ and /boot/config/) so env files stored next
  to stacks are accepted
- Test now uses a temp dir under compose_root instead of /mnt/,
  making it work on all platforms (Linux CI, Windows, macOS)
  without skipping
@mstrhakr mstrhakr merged commit 849a8a6 into mstrhakr:main Feb 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments