Summary
The fix for CVE-2025-66213 added validateShellSafePath and escapeshellarg only for path uses that occur after building the initial command list in LocalFileVolume::saveStorageOnServer(). The vulnerable interpolations are on lines 132 and 139, where $this->fs_path (and $parent_dir derived from it) are pushed into shell commands without escape. Those commands are then sent verbatim via instant_remote_process() over SSH.
Additionally, the file mount flow (submitFileStorage()) does not call validateShellSafePath on the user-controlled path before creating the volume. On save, the model's booted() dispatches ServerStorageSaveJob, which calls saveStorageOnServer() — so RCE is achievable on save, without any deploy.
Affected Versions
< v4.x @ 89aecc2 (confirmed) >
Root Cause
1) Commands built with unescaped fs_path before any hardening
In app/Models/LocalFileVolume.php, saveStorageOnServer() builds $commands as follows:
- Lines 132–134:
$commands->push("mkdir -p $this->fs_path ...");
$commands->push("mkdir -p $workdir ...");
$commands->push("cd $workdir");
- Line 139:
$commands->push("mkdir -p $parent_dir ...");
where $parent_dir is derived from $this->fs_path (line 136).
All of these interpolate user-controlled or path-derived values without escapeshellarg or validation at this point.
2) Validation and escape happen only later, for different uses
At lines 151–152 the code does:
validateShellSafePath($path, 'storage path');
$escapedPath = escapeshellarg($path);
$path here is the transformed path (e.g. with workdir prepended). This $escapedPath is used only for subsequent commands (test -f, cat, tee, touch, mkdir -p {$escapedPath}, etc.). The commands already pushed at 132 and 139 are never rewritten with the escaped value; they still contain raw $this->fs_path and $parent_dir.
3) Commands sent verbatim over SSH
At line 196, instant_remote_process($commands, $server) sends the full $commands array to the remote server. The first elements of $commands therefore execute with unescaped fs_path in the shell, enabling command injection if fs_path contains metacharacters (e.g. ;, |, $(...)).
4) File mount path not validated on submit
In app/Livewire/Project/Service/Storage.php:
submitFileStorageDirectory() (lines 183–184) calls validateShellSafePath() on source and destination before creating the volume.
submitFileStorage() (lines 128–164) only validates 'file_storage_path' => 'required|string' and does not call validateShellSafePath for the file-mount path.
The file path is built as $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path (or equivalent for standalone). The user-controlled suffix (file_storage_path) is stored as fs_path on LocalFileVolume and is then used in the vulnerable command construction in saveStorageOnServer().
5) RCE on save, no deploy required
LocalFileVolume::booted() (lines 26–29) dispatches ServerStorageSaveJob on created. The job's handle() calls saveStorageOnServer(). So creating a new file mount via the UI (submitFileStorage) immediately triggers remote execution of the built commands, including the unescaped fs_path — RCE on save, without any deployment step.
Attack Scenario
- Attacker has permission to add file storage to an application or standalone service (e.g. “File” mount in Storage UI).
- Attacker submits a path suffix containing shell metacharacters (e.g.
/x; id > /tmp/pwned # or $(curl ...)).
submitFileStorage() builds fs_path and creates LocalFileVolume without validateShellSafePath.
ServerStorageSaveJob runs and calls saveStorageOnServer().
- Commands at lines 132 and 139 are built with the malicious
fs_path and executed via instant_remote_process() on the target server → arbitrary command execution.
Impact
- Remote command execution on the server that runs Coolify’s queue and SSH (e.g. Coolify host or deployment target), in the context of
instant_remote_process().
- Potential full compromise of deployment infrastructure, credential theft, and lateral movement.
Suggested Fix
- Use escaped path for all shell uses of
fs_path in saveStorageOnServer()
- Compute a single normalized path (including workdir logic), run
validateShellSafePath() and escapeshellarg() on it once, and use this escaped value in every command that includes the path (including the initial mkdir -p and mkdir -p $parent_dir at lines 132 and 139). Do not push commands that interpolate raw $this->fs_path or $parent_dir.
- Validate file-mount path at submit
- In
submitFileStorage(), after deriving the full path (or the user-controlled segment), call validateShellSafePath() before calling LocalFileVolume::create(), consistent with submitFileStorageDirectory().
- Regression tests
- Add tests that assert metacharacter payloads in
fs_path (and in the file-mount path input) never reach shell execution unescaped, for both file and directory mount flows and for create/update/save paths.
Summary
The fix for CVE-2025-66213 added
validateShellSafePathandescapeshellargonly for path uses that occur after building the initial command list inLocalFileVolume::saveStorageOnServer(). The vulnerable interpolations are on lines 132 and 139, where$this->fs_path(and$parent_dirderived from it) are pushed into shell commands without escape. Those commands are then sent verbatim viainstant_remote_process()over SSH.Additionally, the file mount flow (
submitFileStorage()) does not callvalidateShellSafePathon the user-controlled path before creating the volume. On save, the model'sbooted()dispatchesServerStorageSaveJob, which callssaveStorageOnServer()— so RCE is achievable on save, without any deploy.Affected Versions
< v4.x @ 89aecc2 (confirmed) >Root Cause
1) Commands built with unescaped
fs_pathbefore any hardeningIn
app/Models/LocalFileVolume.php,saveStorageOnServer()builds$commandsas follows:$commands->push("mkdir -p $this->fs_path ...");$commands->push("mkdir -p $workdir ...");$commands->push("cd $workdir");$commands->push("mkdir -p $parent_dir ...");where
$parent_diris derived from$this->fs_path(line 136).All of these interpolate user-controlled or path-derived values without
escapeshellargor validation at this point.2) Validation and escape happen only later, for different uses
At lines 151–152 the code does:
validateShellSafePath($path, 'storage path');$escapedPath = escapeshellarg($path);$pathhere is the transformed path (e.g. with workdir prepended). This$escapedPathis used only for subsequent commands (test -f,cat,tee,touch,mkdir -p {$escapedPath}, etc.). The commands already pushed at 132 and 139 are never rewritten with the escaped value; they still contain raw$this->fs_pathand$parent_dir.3) Commands sent verbatim over SSH
At line 196,
instant_remote_process($commands, $server)sends the full$commandsarray to the remote server. The first elements of$commandstherefore execute with unescapedfs_pathin the shell, enabling command injection iffs_pathcontains metacharacters (e.g.;,|,$(...)).4) File mount path not validated on submit
In
app/Livewire/Project/Service/Storage.php:submitFileStorageDirectory()(lines 183–184) callsvalidateShellSafePath()on source and destination before creating the volume.submitFileStorage()(lines 128–164) only validates'file_storage_path' => 'required|string'and does not callvalidateShellSafePathfor the file-mount path.The file path is built as
$fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path(or equivalent for standalone). The user-controlled suffix (file_storage_path) is stored asfs_pathonLocalFileVolumeand is then used in the vulnerable command construction insaveStorageOnServer().5) RCE on save, no deploy required
LocalFileVolume::booted()(lines 26–29) dispatchesServerStorageSaveJoboncreated. The job'shandle()callssaveStorageOnServer(). So creating a new file mount via the UI (submitFileStorage) immediately triggers remote execution of the built commands, including the unescapedfs_path— RCE on save, without any deployment step.Attack Scenario
/x; id > /tmp/pwned #or$(curl ...)).submitFileStorage()buildsfs_pathand createsLocalFileVolumewithoutvalidateShellSafePath.ServerStorageSaveJobruns and callssaveStorageOnServer().fs_pathand executed viainstant_remote_process()on the target server → arbitrary command execution.Impact
instant_remote_process().Suggested Fix
fs_pathinsaveStorageOnServer()validateShellSafePath()andescapeshellarg()on it once, and use this escaped value in every command that includes the path (including the initialmkdir -pandmkdir -p $parent_dirat lines 132 and 139). Do not push commands that interpolate raw$this->fs_pathor$parent_dir.submitFileStorage(), after deriving the full path (or the user-controlled segment), callvalidateShellSafePath()before callingLocalFileVolume::create(), consistent withsubmitFileStorageDirectory().fs_path(and in the file-mount path input) never reach shell execution unescaped, for both file and directory mount flows and for create/update/save paths.