mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 19:21:36 +00:00
Add ValidProxyConfigFilename rule for dynamic proxy config validation (#7544)
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Livewire\Server\Proxy;
|
|||||||
|
|
||||||
use App\Enums\ProxyTypes;
|
use App\Enums\ProxyTypes;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Rules\ValidProxyConfigFilename;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
@@ -38,11 +39,11 @@ class NewDynamicConfiguration extends Component
|
|||||||
try {
|
try {
|
||||||
$this->authorize('update', $this->server);
|
$this->authorize('update', $this->server);
|
||||||
$this->validate([
|
$this->validate([
|
||||||
'fileName' => 'required',
|
'fileName' => ['required', new ValidProxyConfigFilename],
|
||||||
'value' => 'required',
|
'value' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Validate filename to prevent command injection
|
// Additional security validation to prevent command injection
|
||||||
validateShellSafePath($this->fileName, 'proxy configuration filename');
|
validateShellSafePath($this->fileName, 'proxy configuration filename');
|
||||||
|
|
||||||
if (data_get($this->parameters, 'server_uuid')) {
|
if (data_get($this->parameters, 'server_uuid')) {
|
||||||
|
|||||||
73
app/Rules/ValidProxyConfigFilename.php
Normal file
73
app/Rules/ValidProxyConfigFilename.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
class ValidProxyConfigFilename implements ValidationRule
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Reserved filenames that cannot be used.
|
||||||
|
*/
|
||||||
|
private const RESERVED_FILENAMES = [
|
||||||
|
'coolify.yaml',
|
||||||
|
'coolify.yml',
|
||||||
|
'Caddyfile',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the validation rule.
|
||||||
|
*
|
||||||
|
* Validates proxy configuration filename:
|
||||||
|
* - Must be 1-255 characters
|
||||||
|
* - No path separators (/, \) to prevent path traversal
|
||||||
|
* - Cannot start with a dot (hidden files)
|
||||||
|
* - Only alphanumeric characters, dashes, underscores, and dots allowed
|
||||||
|
* - Must have a basename before any extension
|
||||||
|
* - Cannot use reserved filenames
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (empty($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$filename = trim($value);
|
||||||
|
|
||||||
|
// Check length (filesystem limit is typically 255 bytes)
|
||||||
|
if (strlen($filename) > 255) {
|
||||||
|
$fail('The :attribute must not exceed 255 characters.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for path separators (prevent path traversal)
|
||||||
|
if (str_contains($filename, '/') || str_contains($filename, '\\')) {
|
||||||
|
$fail('The :attribute cannot contain path separators.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for hidden files (starting with dot)
|
||||||
|
if (str_starts_with($filename, '.')) {
|
||||||
|
$fail('The :attribute cannot start with a dot (hidden files not allowed).');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid characters only: alphanumeric, dashes, underscores, dots
|
||||||
|
if (! preg_match('/^[a-zA-Z0-9._-]+$/', $filename)) {
|
||||||
|
$fail('The :attribute may only contain letters, numbers, dashes, underscores, and dots.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for reserved filenames (case-sensitive for coolify.yaml/yml, case-insensitive check not needed as Caddyfile is exact)
|
||||||
|
if (in_array($filename, self::RESERVED_FILENAMES, true)) {
|
||||||
|
$fail('The :attribute uses a reserved filename.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
185
tests/Unit/ValidProxyConfigFilenameTest.php
Normal file
185
tests/Unit/ValidProxyConfigFilenameTest.php
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Rules\ValidProxyConfigFilename;
|
||||||
|
|
||||||
|
test('allows valid proxy config filenames', function () {
|
||||||
|
$validFilenames = [
|
||||||
|
'my-config',
|
||||||
|
'service_name.yaml',
|
||||||
|
'router-1.yml',
|
||||||
|
'traefik-config',
|
||||||
|
'my.service.yaml',
|
||||||
|
'config_v2.caddy',
|
||||||
|
'API-Gateway.yaml',
|
||||||
|
'load-balancer_prod.yml',
|
||||||
|
];
|
||||||
|
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failures = [];
|
||||||
|
|
||||||
|
foreach ($validFilenames as $filename) {
|
||||||
|
$rule->validate('fileName', $filename, function ($message) use (&$failures, $filename) {
|
||||||
|
$failures[] = "{$filename}: {$message}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($failures)->toBeEmpty();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks path traversal with forward slash', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', '../etc/passwd', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks path traversal with backslash', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', '..\\windows\\system32', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks hidden files starting with dot', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', '.hidden.yaml', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks reserved filename coolify.yaml', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', 'coolify.yaml', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks reserved filename coolify.yml', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', 'coolify.yml', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks reserved filename Caddyfile', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', 'Caddyfile', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks filenames with invalid characters', function () {
|
||||||
|
$invalidFilenames = [
|
||||||
|
'file;rm.yaml',
|
||||||
|
'file|test.yaml',
|
||||||
|
'config$var.yaml',
|
||||||
|
'test`cmd`.yaml',
|
||||||
|
'name with spaces.yaml',
|
||||||
|
'file<redirect.yaml',
|
||||||
|
'file>output.yaml',
|
||||||
|
'config&background.yaml',
|
||||||
|
"file\nnewline.yaml",
|
||||||
|
];
|
||||||
|
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
|
||||||
|
foreach ($invalidFilenames as $filename) {
|
||||||
|
$failed = false;
|
||||||
|
$rule->validate('fileName', $filename, function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue("Expected '{$filename}' to be rejected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks filenames exceeding 255 characters', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$longFilename = str_repeat('a', 256);
|
||||||
|
$rule->validate('fileName', $longFilename, function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows filenames at exactly 255 characters', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$exactFilename = str_repeat('a', 255);
|
||||||
|
$rule->validate('fileName', $exactFilename, function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows empty values without failing', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', '', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('blocks nested path traversal', function () {
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failed = false;
|
||||||
|
|
||||||
|
$rule->validate('fileName', 'foo/bar/../../etc/passwd', function () use (&$failed) {
|
||||||
|
$failed = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect($failed)->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('allows similar but not reserved filenames', function () {
|
||||||
|
$validFilenames = [
|
||||||
|
'coolify-custom.yaml',
|
||||||
|
'my-coolify.yaml',
|
||||||
|
'coolify2.yaml',
|
||||||
|
'Caddyfile.backup',
|
||||||
|
'my-Caddyfile',
|
||||||
|
];
|
||||||
|
|
||||||
|
$rule = new ValidProxyConfigFilename;
|
||||||
|
$failures = [];
|
||||||
|
|
||||||
|
foreach ($validFilenames as $filename) {
|
||||||
|
$rule->validate('fileName', $filename, function ($message) use (&$failures, $filename) {
|
||||||
|
$failures[] = "{$filename}: {$message}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($failures)->toBeEmpty();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user