coolify/app/Livewire/Server/Proxy/NewDynamicConfiguration.php
Andras Bacsai 028fb5c22c Add ValidProxyConfigFilename rule for dynamic proxy config validation
Adds a new Laravel validation rule to prevent path traversal, hidden files, and invalid filenames in the dynamic proxy configuration feature. Validates filenames to ensure they contain only safe characters, don't exceed filesystem limits, and don't use reserved names.

- New Rule: ValidProxyConfigFilename with comprehensive validation
- Updated: NewDynamicConfiguration to use the new rule
- Added: 13 unit tests covering all validation scenarios

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 16:12:45 +01:00

107 lines
3.6 KiB
PHP

<?php
namespace App\Livewire\Server\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server;
use App\Rules\ValidProxyConfigFilename;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
use Symfony\Component\Yaml\Yaml;
class NewDynamicConfiguration extends Component
{
use AuthorizesRequests;
public string $fileName = '';
public string $value = '';
public bool $newFile = false;
public Server $server;
public $server_id;
public $parameters = [];
public function mount()
{
$this->server = Server::ownedByCurrentTeam()->whereId($this->server_id)->first();
$this->parameters = get_route_parameters();
if ($this->fileName !== '') {
$this->fileName = str_replace('|', '.', $this->fileName);
}
}
public function addDynamicConfiguration()
{
try {
$this->authorize('update', $this->server);
$this->validate([
'fileName' => ['required', new ValidProxyConfigFilename],
'value' => 'required',
]);
// Additional security validation to prevent command injection
validateShellSafePath($this->fileName, 'proxy configuration filename');
if (data_get($this->parameters, 'server_uuid')) {
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
}
if (is_null($this->server)) {
return redirect()->route('server.index');
}
$proxy_type = $this->server->proxyType();
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
if (! str($this->fileName)->endsWith('.yaml') && ! str($this->fileName)->endsWith('.yml')) {
$this->fileName = "{$this->fileName}.yaml";
}
if ($this->fileName === 'coolify.yaml') {
$this->dispatch('error', 'File name is reserved.');
return;
}
} elseif ($proxy_type === 'CADDY') {
if (! str($this->fileName)->endsWith('.caddy')) {
$this->fileName = "{$this->fileName}.caddy";
}
}
$proxy_path = $this->server->proxyPath();
$file = "{$proxy_path}/dynamic/{$this->fileName}";
$escapedFile = escapeshellarg($file);
if ($this->newFile) {
$exists = instant_remote_process(["test -f {$escapedFile} && echo 1 || echo 0"], $this->server);
if ($exists == 1) {
$this->dispatch('error', 'File already exists');
return;
}
}
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$yaml = Yaml::parse($this->value);
$yaml = Yaml::dump($yaml, 10, 2);
$this->value = $yaml;
}
$base64_value = base64_encode($this->value);
instant_remote_process([
"echo '{$base64_value}' | base64 -d | tee {$escapedFile} > /dev/null",
], $this->server);
if ($proxy_type === 'CADDY') {
$this->server->reloadCaddy();
}
$this->dispatch('loadDynamicConfigurations');
$this->dispatch('dynamic-configuration-added');
$this->dispatch('success', 'Dynamic configuration saved.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.new-dynamic-configuration');
}
}