mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 18:15:49 +00:00
fix(env): validate Docker-compatible variable keys
Add shared environment variable key validation and normalization for Livewire forms and models, allowing Docker-compatible keys while rejecting invalid entries such as keys containing equals signs. Also quote Railpack build environment and secret arguments safely.
This commit is contained in:
1 parent
d5946dcfca
commit
b5ff124446
11 files changed
+294
-57
No files matched your search
@@ -2570,7 +2570,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
return 'env '.$variables
|
||||
->map(function ($value, $key) {
|
||||
return "{$key}=".escapeShellValue($value);
|
||||
return escapeShellValue("{$key}={$value}");
|
||||
})
|
||||
->implode(' ').' ';
|
||||
}
|
||||
@@ -2583,7 +2583,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
return ' '.$variables
|
||||
->map(function ($value, $key) {
|
||||
return "--secret id={$key},env={$key}";
|
||||
return '--secret '.escapeShellValue("id={$key},env={$key}");
|
||||
})
|
||||
->implode(' ');
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\EnvironmentVariableAnalyzer;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
@@ -37,15 +42,23 @@ class Add extends Component
|
||||
|
||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||
|
||||
protected $rules = [
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
];
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
||||
'value' => 'nullable',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
];
|
||||
}
|
||||
|
||||
protected function messages(): array
|
||||
{
|
||||
return ValidationPatterns::environmentVariableKeyMessages('key');
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'key' => 'key',
|
||||
@@ -85,7 +98,7 @@ class Add extends Component
|
||||
$result['team'] = $team->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view team variables
|
||||
}
|
||||
|
||||
@@ -116,12 +129,12 @@ class Add extends Component
|
||||
$result['environment'] = $environment->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view environment variables
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view project variables
|
||||
}
|
||||
}
|
||||
@@ -131,7 +144,7 @@ class Add extends Component
|
||||
$serverUuid = data_get($this->parameters, 'server_uuid');
|
||||
if ($serverUuid) {
|
||||
// If we have a specific server_uuid, show variables for that server
|
||||
$server = \App\Models\Server::where('team_id', $team->id)
|
||||
$server = Server::where('team_id', $team->id)
|
||||
->where('uuid', $serverUuid)
|
||||
->first();
|
||||
|
||||
@@ -141,7 +154,7 @@ class Add extends Component
|
||||
$result['server'] = $server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
@@ -149,7 +162,7 @@ class Add extends Component
|
||||
// For application environment variables, try to use the application's destination server
|
||||
$applicationUuid = data_get($this->parameters, 'application_uuid');
|
||||
if ($applicationUuid) {
|
||||
$application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id)
|
||||
$application = Application::whereRelation('environment.project.team', 'id', $team->id)
|
||||
->where('uuid', $applicationUuid)
|
||||
->with('destination.server')
|
||||
->first();
|
||||
@@ -160,7 +173,7 @@ class Add extends Component
|
||||
$result['server'] = $application->destination->server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
@@ -168,7 +181,7 @@ class Add extends Component
|
||||
// For service environment variables, try to use the service's server
|
||||
$serviceUuid = data_get($this->parameters, 'service_uuid');
|
||||
if ($serviceUuid) {
|
||||
$service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id)
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $team->id)
|
||||
->where('uuid', $serviceUuid)
|
||||
->with('server')
|
||||
->first();
|
||||
@@ -179,7 +192,7 @@ class Add extends Component
|
||||
$result['server'] = $service->server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
@@ -192,6 +205,7 @@ class Add extends Component
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
||||
$this->validate();
|
||||
$this->dispatch('saveKey', [
|
||||
'key' => $this->key,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
@@ -38,7 +40,7 @@ class All extends Component
|
||||
$this->is_env_sorting_enabled = data_get($this->resource, 'settings.is_env_sorting_enabled', false);
|
||||
$this->use_build_secrets = data_get($this->resource, 'settings.use_build_secrets', false);
|
||||
$this->resourceClass = get_class($this->resource);
|
||||
$resourceWithPreviews = [\App\Models\Application::class];
|
||||
$resourceWithPreviews = [Application::class];
|
||||
$simpleDockerfile = filled(data_get($this->resource, 'dockerfile'));
|
||||
if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) {
|
||||
$this->showPreview = true;
|
||||
@@ -194,7 +196,7 @@ class All extends Component
|
||||
|
||||
private function updateOrder()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$variables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variables));
|
||||
$order = 1;
|
||||
foreach ($variables as $key => $value) {
|
||||
$env = $this->resource->environment_variables()->where('key', $key)->first();
|
||||
@@ -206,7 +208,7 @@ class All extends Component
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$previewVariables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variablesPreview));
|
||||
$order = 1;
|
||||
foreach ($previewVariables as $key => $value) {
|
||||
$env = $this->resource->environment_variables_preview()->where('key', $key)->first();
|
||||
@@ -221,7 +223,7 @@ class All extends Component
|
||||
|
||||
private function handleBulkSubmit()
|
||||
{
|
||||
$variables = parseEnvFormatToArray($this->variables);
|
||||
$variables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variables));
|
||||
$changesMade = false;
|
||||
$errorOccurred = false;
|
||||
|
||||
@@ -241,7 +243,7 @@ class All extends Component
|
||||
}
|
||||
|
||||
if ($this->showPreview) {
|
||||
$previewVariables = parseEnvFormatToArray($this->variablesPreview);
|
||||
$previewVariables = $this->normalizeEnvironmentVariables(parseEnvFormatToArray($this->variablesPreview));
|
||||
|
||||
// Try to delete removed preview variables
|
||||
$deletedPreviewCount = $this->deleteRemovedVariables(true, $previewVariables);
|
||||
@@ -267,6 +269,7 @@ class All extends Component
|
||||
|
||||
private function handleSingleSubmit($data)
|
||||
{
|
||||
$data['key'] = ValidationPatterns::validatedEnvironmentVariableKey($data['key']);
|
||||
$found = $this->resource->environment_variables()->where('key', $data['key'])->first();
|
||||
if ($found) {
|
||||
$this->dispatch('error', 'Environment variable already exists.');
|
||||
@@ -334,6 +337,23 @@ class All extends Component
|
||||
return $variablesToDelete->count();
|
||||
}
|
||||
|
||||
private function normalizeEnvironmentVariables(array $variables): array
|
||||
{
|
||||
$normalizedVariables = [];
|
||||
|
||||
foreach ($variables as $key => $data) {
|
||||
$normalizedKey = ValidationPatterns::validatedEnvironmentVariableKey((string) $key);
|
||||
|
||||
if (array_key_exists($normalizedKey, $normalizedVariables)) {
|
||||
throw new \InvalidArgumentException("Duplicate environment variable key after normalization: {$normalizedKey}.");
|
||||
}
|
||||
|
||||
$normalizedVariables[$normalizedKey] = $data;
|
||||
}
|
||||
|
||||
return $normalizedVariables;
|
||||
}
|
||||
|
||||
private function updateOrCreateVariables($isPreview, $variables)
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
@@ -2,12 +2,17 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Support\ValidationPatterns;
|
||||
use App\Traits\EnvironmentVariableAnalyzer;
|
||||
use App\Traits\EnvironmentVariableProtection;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Computed;
|
||||
use Livewire\Component;
|
||||
@@ -64,23 +69,31 @@ class Show extends Component
|
||||
'compose_loaded' => '$refresh',
|
||||
];
|
||||
|
||||
protected $rules = [
|
||||
'key' => 'required|string',
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'real_value' => 'nullable',
|
||||
'is_required' => 'required|boolean',
|
||||
];
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
'is_literal' => 'required|boolean',
|
||||
'is_shown_once' => 'required|boolean',
|
||||
'is_runtime' => 'required|boolean',
|
||||
'is_buildtime' => 'required|boolean',
|
||||
'real_value' => 'nullable',
|
||||
'is_required' => 'required|boolean',
|
||||
];
|
||||
}
|
||||
|
||||
protected function messages(): array
|
||||
{
|
||||
return ValidationPatterns::environmentVariableKeyMessages('key');
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData();
|
||||
if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) {
|
||||
if ($this->env->getMorphClass() === SharedEnvironmentVariable::class) {
|
||||
$this->isSharedVariable = true;
|
||||
}
|
||||
$this->parameters = get_route_parameters();
|
||||
@@ -108,9 +121,11 @@ class Show extends Component
|
||||
public function syncData(bool $toModel = false)
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->key = ValidationPatterns::normalizeEnvironmentVariableKey($this->key);
|
||||
|
||||
if ($this->isSharedVariable) {
|
||||
$this->validate([
|
||||
'key' => 'required|string',
|
||||
'key' => ValidationPatterns::environmentVariableKeyRules(),
|
||||
'value' => 'nullable',
|
||||
'comment' => 'nullable|string|max:256',
|
||||
'is_multiline' => 'required|boolean',
|
||||
@@ -233,7 +248,7 @@ class Show extends Component
|
||||
$result['team'] = $team->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view team variables
|
||||
}
|
||||
|
||||
@@ -264,12 +279,12 @@ class Show extends Component
|
||||
$result['environment'] = $environment->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view environment variables
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view project variables
|
||||
}
|
||||
}
|
||||
@@ -279,7 +294,7 @@ class Show extends Component
|
||||
$serverUuid = data_get($this->parameters, 'server_uuid');
|
||||
if ($serverUuid) {
|
||||
// If we have a specific server_uuid, show variables for that server
|
||||
$server = \App\Models\Server::where('team_id', $team->id)
|
||||
$server = Server::where('team_id', $team->id)
|
||||
->where('uuid', $serverUuid)
|
||||
->first();
|
||||
|
||||
@@ -289,7 +304,7 @@ class Show extends Component
|
||||
$result['server'] = $server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
@@ -297,7 +312,7 @@ class Show extends Component
|
||||
// For application environment variables, try to use the application's destination server
|
||||
$applicationUuid = data_get($this->parameters, 'application_uuid');
|
||||
if ($applicationUuid) {
|
||||
$application = \App\Models\Application::whereRelation('environment.project.team', 'id', $team->id)
|
||||
$application = Application::whereRelation('environment.project.team', 'id', $team->id)
|
||||
->where('uuid', $applicationUuid)
|
||||
->with('destination.server')
|
||||
->first();
|
||||
@@ -308,7 +323,7 @@ class Show extends Component
|
||||
$result['server'] = $application->destination->server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
@@ -316,7 +331,7 @@ class Show extends Component
|
||||
// For service environment variables, try to use the service's server
|
||||
$serviceUuid = data_get($this->parameters, 'service_uuid');
|
||||
if ($serviceUuid) {
|
||||
$service = \App\Models\Service::whereRelation('environment.project.team', 'id', $team->id)
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $team->id)
|
||||
->where('uuid', $serviceUuid)
|
||||
->with('server')
|
||||
->first();
|
||||
@@ -327,7 +342,7 @@ class Show extends Component
|
||||
$result['server'] = $service->server->environment_variables()
|
||||
->pluck('key')
|
||||
->toArray();
|
||||
} catch (\Illuminate\Auth\Access\AuthorizationException $e) {
|
||||
} catch (AuthorizationException $e) {
|
||||
// User not authorized to view server variables
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -370,7 +371,7 @@ class EnvironmentVariable extends BaseModel
|
||||
protected function key(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
|
||||
set: fn (string $value) => ValidationPatterns::validatedEnvironmentVariableKey($value),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SharedEnvironmentVariable extends Model
|
||||
@@ -33,6 +35,13 @@ class SharedEnvironmentVariable extends Model
|
||||
'value' => 'encrypted',
|
||||
];
|
||||
|
||||
protected function key(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
set: fn (string $value) => ValidationPatterns::validatedEnvironmentVariableKey($value),
|
||||
);
|
||||
}
|
||||
|
||||
public function team()
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
|
||||
@@ -82,6 +82,12 @@ class ValidationPatterns
|
||||
*/
|
||||
public const DOCKER_NETWORK_PATTERN = '/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/';
|
||||
|
||||
/**
|
||||
* Pattern for Docker-compatible environment variable keys.
|
||||
* Docker environment entries are KEY=value strings, so keys must be non-empty and cannot contain '=' or NUL.
|
||||
*/
|
||||
public const ENVIRONMENT_VARIABLE_KEY_PATTERN = '/\A[^=\x00]+\z/u';
|
||||
|
||||
/**
|
||||
* Pattern for SQL-safe unquoted database identifiers (usernames, database names).
|
||||
* Allows letters, digits, underscore; first char must be letter or underscore.
|
||||
@@ -96,6 +102,67 @@ class ValidationPatterns
|
||||
*/
|
||||
public const DB_PASSWORD_PATTERN = '/^[A-Za-z0-9!@#%^*()_+\-=\[\]{}:,.?\/~]+$/';
|
||||
|
||||
/**
|
||||
* Normalize environment variable keys before validation and storage.
|
||||
*/
|
||||
public static function normalizeEnvironmentVariableKey(string $value): string
|
||||
{
|
||||
return str($value)->trim()->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules for environment variable keys.
|
||||
*/
|
||||
public static function environmentVariableKeyRules(bool $required = true, int $maxLength = 255): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if ($required) {
|
||||
$rules[] = 'required';
|
||||
} else {
|
||||
$rules[] = 'nullable';
|
||||
}
|
||||
|
||||
$rules[] = 'string';
|
||||
$rules[] = "max:$maxLength";
|
||||
$rules[] = 'regex:'.self::ENVIRONMENT_VARIABLE_KEY_PATTERN;
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation messages for environment variable key fields.
|
||||
*/
|
||||
public static function environmentVariableKeyMessages(string $field = 'key', string $label = 'key'): array
|
||||
{
|
||||
return [
|
||||
"{$field}.regex" => "The {$label} must be a non-empty Docker-compatible environment variable key and cannot contain '=' or NUL characters.",
|
||||
"{$field}.max" => "The {$label} may not be greater than :max characters.",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string is a valid environment variable key.
|
||||
*/
|
||||
public static function isValidEnvironmentVariableKey(string $value): bool
|
||||
{
|
||||
return preg_match(self::ENVIRONMENT_VARIABLE_KEY_PATTERN, $value) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize and validate an environment variable key.
|
||||
*/
|
||||
public static function validatedEnvironmentVariableKey(string $value, string $label = 'key'): string
|
||||
{
|
||||
$key = self::normalizeEnvironmentVariableKey($value);
|
||||
|
||||
if (! self::isValidEnvironmentVariableKey($key)) {
|
||||
throw new \InvalidArgumentException(self::environmentVariableKeyMessages(label: $label)['key.regex']);
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get validation rules for database identifier fields (username, database name).
|
||||
*
|
||||
|
||||
Reference in new issue
Block a user