mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 21:18:11 -04:00
Merge remote-tracking branch 'origin/main' into next
This commit is contained in:
@@ -22,11 +22,10 @@ use Symfony\Component\Yaml\Yaml;
|
||||
* This should be called BEFORE saving to database to prevent malicious data from being stored.
|
||||
*
|
||||
* @param string $composeYaml The raw Docker Compose YAML content
|
||||
* @param string|null $resourceDirectory The resource directory, if the resource exists (see validateComposeContentVolumeSource())
|
||||
*
|
||||
* @throws Exception If the compose file contains command injection attempts
|
||||
*/
|
||||
function validateDockerComposeForInjection(string $composeYaml, ?string $resourceDirectory = null): void
|
||||
function validateDockerComposeForInjection(string $composeYaml): void
|
||||
{
|
||||
try {
|
||||
$parsed = Yaml::parse($composeYaml);
|
||||
@@ -79,7 +78,7 @@ function validateDockerComposeForInjection(string $composeYaml, ?string $resourc
|
||||
}
|
||||
}
|
||||
}
|
||||
validateComposeContentVolumeSource($volume, $resourceDirectory);
|
||||
validateComposeContentVolumeSource($volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,55 +129,27 @@ function composeResourceDirectory(Application|Service $resource): string
|
||||
}
|
||||
|
||||
/**
|
||||
* Coolify writes the `content:` of a Compose bind volume to the host, so that file must be inside
|
||||
* the resource directory. The source must be a `./` path that stays inside the resource directory,
|
||||
* or an absolute path inside $resourceDirectory. Sources with `~`, `..` or variables are rejected,
|
||||
* because their host path is not known before the write. Bind volumes without `content:` are not
|
||||
* changed: an administrator can mount any host path.
|
||||
* Coolify writes the `content:` of a Compose bind volume to the source path on the host. Only
|
||||
* administrators can edit a Compose file, and they can mount any host path, so the source can be
|
||||
* any path, also outside the resource directory. It must exist and be safe to
|
||||
* use in a shell command; Coolify escapes it in every remote command.
|
||||
*
|
||||
* @param array<string, mixed> $volume A long-syntax Compose volume
|
||||
*
|
||||
* @throws Exception If Coolify would write the content outside the resource directory
|
||||
* @throws Exception If the source is missing or contains shell metacharacters
|
||||
*/
|
||||
function validateComposeContentVolumeSource(array $volume, ?string $resourceDirectory = null): void
|
||||
function validateComposeContentVolumeSource(array $volume): void
|
||||
{
|
||||
if (! array_key_exists('content', $volume) || ($volume['type'] ?? null) !== 'bind') {
|
||||
return;
|
||||
}
|
||||
|
||||
$source = $volume['source'] ?? null;
|
||||
$displaySource = is_scalar($source) && (string) $source !== '' ? (string) $source : '(empty)';
|
||||
$error = new Exception(
|
||||
"Volume source {$displaySource} with content must be inside the resource directory. Use a relative path such as ./config/app.conf."
|
||||
);
|
||||
|
||||
if (! is_string($source) || str_contains($source, '$') || str_contains($source, '\\')) {
|
||||
throw $error;
|
||||
}
|
||||
if (in_array('..', explode('/', $source), true)) {
|
||||
throw $error;
|
||||
if (! is_string($source) || trim($source) === '') {
|
||||
throw new Exception('Invalid Docker volume definition (array syntax): A bind volume with content needs a source path.');
|
||||
}
|
||||
|
||||
if (str_starts_with($source, './')) {
|
||||
$baseDirectory = $resourceDirectory ?? '/coolify-resource-directory';
|
||||
$path = $baseDirectory.'/'.substr($source, 2);
|
||||
} elseif (str_starts_with($source, '/') && $resourceDirectory !== null) {
|
||||
$baseDirectory = $resourceDirectory;
|
||||
$path = $source;
|
||||
} else {
|
||||
throw $error;
|
||||
}
|
||||
|
||||
try {
|
||||
$baseDirectory = normalizeUnixPath($baseDirectory);
|
||||
$path = normalizeUnixPath($path);
|
||||
} catch (Exception) {
|
||||
throw $error;
|
||||
}
|
||||
|
||||
if (! str_starts_with($path, $baseDirectory.'/')) {
|
||||
throw $error;
|
||||
}
|
||||
validateComposeArrayVolumeSource($source);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1419,7 +1390,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
if ($source !== null && ! empty($source->value())) {
|
||||
validateComposeArrayVolumeSource($source->value());
|
||||
}
|
||||
validateComposeContentVolumeSource($volume, composeResourceDirectory($resource));
|
||||
validateComposeContentVolumeSource($volume);
|
||||
if ($target !== null && ! empty($target->value())) {
|
||||
try {
|
||||
validateShellSafePath($target->value(), 'volume target');
|
||||
@@ -2791,7 +2762,7 @@ function serviceParser(Service $resource): Collection
|
||||
if ($source !== null && ! empty($source->value())) {
|
||||
validateComposeArrayVolumeSource($source->value());
|
||||
}
|
||||
validateComposeContentVolumeSource($volume, composeResourceDirectory($resource));
|
||||
validateComposeContentVolumeSource($volume);
|
||||
if ($target !== null && ! empty($target->value())) {
|
||||
try {
|
||||
validateShellSafePath($target->value(), 'volume target');
|
||||
|
||||
@@ -204,14 +204,11 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
|
||||
]);
|
||||
instant_remote_process($commands, $server);
|
||||
foreach ($fileVolumes as $fileVolume) {
|
||||
$path = str(data_get($fileVolume, 'fs_path'));
|
||||
$content = data_get($fileVolume, 'content');
|
||||
if ($path->startsWith('.')) {
|
||||
$path = $path->after('.');
|
||||
$fileLocation = $workdir.$path;
|
||||
} else {
|
||||
$fileLocation = $path;
|
||||
}
|
||||
// A relative path is inside the resource directory, never in the SSH user's working
|
||||
// directory. A persisted Compose expression (`${VAR:-/path}`) stays as it is.
|
||||
$fsPath = trim((string) $fileVolume->fs_path);
|
||||
$fileLocation = str_starts_with($fsPath, '$') ? $fsPath : $fileVolume->resolvedFsPath($workdir)->value();
|
||||
$escapedFileLocation = filesystemVolumeShellArgument((string) $fileLocation);
|
||||
// Exists and is a file
|
||||
$isFile = instant_remote_process(["test -f {$escapedFileLocation} && echo OK || echo NOK"], $server);
|
||||
@@ -237,16 +234,14 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli
|
||||
$fileVolume->is_directory = true;
|
||||
$fileVolume->save();
|
||||
} elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) {
|
||||
// Does not exists (no dir or file), not flagged as directory, is init, has content.
|
||||
// Content is written only inside the resource directory, as a literal path.
|
||||
$escapedContentLocation = escapeshellarg($fileVolume->confinedContentPath((string) $fileLocation, $server));
|
||||
// Does not exists (no dir or file), not flagged as directory, is init, has content
|
||||
$fileVolume->content = $content;
|
||||
$fileVolume->is_directory = false;
|
||||
$fileVolume->save();
|
||||
$content = base64_encode($content);
|
||||
instant_remote_process([
|
||||
'mkdir -p -- "$(dirname -- '.$escapedContentLocation.')"',
|
||||
"echo '$content' | base64 -d | tee -- {$escapedContentLocation}",
|
||||
'mkdir -p -- "$(dirname -- '.$escapedFileLocation.')"',
|
||||
"echo '$content' | base64 -d | tee -- {$escapedFileLocation}",
|
||||
], $server);
|
||||
} elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) {
|
||||
// Does not exists (no dir or file), flagged as directory, is init
|
||||
|
||||
@@ -2955,7 +2955,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
||||
$target = data_get_str($volume, 'target');
|
||||
$content = data_get($volume, 'content');
|
||||
$isDirectory = (bool) data_get($volume, 'isDirectory', null) || (bool) data_get($volume, 'is_directory', null);
|
||||
validateComposeContentVolumeSource($volume, $savedService->service->workdir());
|
||||
validateComposeContentVolumeSource($volume);
|
||||
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
|
||||
if ($foundConfig) {
|
||||
$contentNotNull = data_get($foundConfig, 'content');
|
||||
|
||||
Reference in New Issue
Block a user