mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Store server IPs in one form (no brackets, short lower-case IPv6) and write each address in the form its context needs: - URLs and host:port (Sentinel, database URLs, SFTP backup download, DNS servers, SMTP, webhook base URLs, links, health checks) get brackets through formatHostForUrl(). - Traefik Host() rules use the address without brackets, so IPv6 domains no longer return 404; saved labels are fixed at deploy. Caddy keeps the brackets. - SSH keeps the plain address. - Hetzner gives an IPv6 /64 network; IPv6-only servers now get the server address (prefix::1) instead of the unusable prefix::64. - sslip labels, registry image names with an IPv6 host, the API domain list, allowlist matching and duplicate checks handle IPv6. A migration normalizes stored IPv6 server addresses, repairs Hetzner servers saved as ::64 that are not reachable, and adds brackets to saved Sentinel URLs.
118 lines
4.8 KiB
PHP
118 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Exceptions\DeploymentException;
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Rules\DockerImageFormat;
|
|
use App\Support\ValidationPatterns;
|
|
|
|
it('accepts valid docker registry image names', function (string $imageName) {
|
|
expect(ValidationPatterns::isValidDockerImageName($imageName))->toBeTrue();
|
|
})->with([
|
|
'single component' => 'nginx',
|
|
'namespace image' => 'library/nginx',
|
|
'ghcr image' => 'ghcr.io/coollabsio/coolify',
|
|
'repository component with repeated hyphens' => 'ghcr.io/acme/my--service',
|
|
'registry with port' => 'registry.example.com:5000/team/app',
|
|
'IPv6 registry' => '[2a01:4f8::1]/team/app',
|
|
'IPv6 registry with port' => '[2a01:4f8::1]:5000/app',
|
|
'IPv4-mapped IPv6 registry' => '[::ffff:192.0.2.1]:5000/app',
|
|
'digest marker used by existing dockerimage records' => 'nginx@sha256',
|
|
]);
|
|
|
|
it('rejects docker registry image names with shell metacharacters', function (string $imageName) {
|
|
expect(ValidationPatterns::isValidDockerImageName($imageName))->toBeFalse();
|
|
})->with([
|
|
'command substitution' => 'coolify/poc$(touch /tmp/pwned)',
|
|
'semicolon' => 'coolify/poc;id',
|
|
'backticks' => 'coolify/poc`id`',
|
|
'pipe' => 'coolify/poc|id',
|
|
'logical and' => 'coolify/poc&&id',
|
|
'newline' => "coolify/poc\nid",
|
|
'space' => 'coolify/poc image',
|
|
'tag in image-name-only field' => 'coolify/poc:latest',
|
|
'IPv6 registry with command substitution' => '[2a01:4f8::1$(id)]:5000/app',
|
|
'IPv6 registry with letters outside hex' => '[2a01:4f8::zz]/app',
|
|
'unclosed IPv6 registry' => '[2a01:4f8::1:5000/app',
|
|
'bare IPv6 registry' => '2a01:4f8::1/app',
|
|
]);
|
|
|
|
it('accepts valid docker registry image tags', function (string $tag) {
|
|
expect(ValidationPatterns::isValidDockerImageTag($tag))->toBeTrue();
|
|
})->with([
|
|
'latest' => 'latest',
|
|
'version' => 'v1.2.3',
|
|
'uppercase and underscore' => 'PR_123',
|
|
'sha256 hash' => '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'legacy sha256 prefixed hash' => 'sha256-1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
]);
|
|
|
|
it('rejects docker registry image tags with shell metacharacters', function (string $tag) {
|
|
expect(ValidationPatterns::isValidDockerImageTag($tag))->toBeFalse();
|
|
})->with([
|
|
'command substitution' => 'latest$(touch /tmp/pwned)',
|
|
'semicolon' => 'latest;id',
|
|
'backticks' => 'latest`id`',
|
|
'pipe' => 'latest|id',
|
|
'logical and' => 'latest&&id',
|
|
'newline' => "latest\nid",
|
|
]);
|
|
|
|
it('accepts supported full docker image reference formats', function (string $imageReference) {
|
|
$failures = [];
|
|
|
|
(new DockerImageFormat)->validate('image', $imageReference, function (string $message) use (&$failures): void {
|
|
$failures[] = $message;
|
|
});
|
|
|
|
expect($failures)->toBeEmpty();
|
|
})->with([
|
|
'image with tag' => 'nginx:latest',
|
|
'registry image with tag' => 'ghcr.io/user/app:v1.2.3',
|
|
'image with sha256 digest' => 'nginx@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'registry image with sha256 digest' => 'ghcr.io/user/app@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'registry port image with tag' => 'localhost:5000/app:latest',
|
|
'IPv6 registry port image with tag' => '[2a01:4f8::1]:5000/app:latest',
|
|
]);
|
|
|
|
it('rejects unsupported full docker image reference formats', function (string $imageReference) {
|
|
$failures = [];
|
|
|
|
(new DockerImageFormat)->validate('image', $imageReference, function (string $message) use (&$failures): void {
|
|
$failures[] = $message;
|
|
});
|
|
|
|
expect($failures)->not->toBeEmpty();
|
|
})->with([
|
|
'colon sha256 marker' => 'nginx:sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
'command substitution' => 'nginx:latest$(touch /tmp/pwned)',
|
|
'newline' => "nginx:latest\nid",
|
|
]);
|
|
|
|
it('stops deployments when a stored docker registry image value is unsafe', function () {
|
|
$job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor();
|
|
|
|
$application = new Application([
|
|
'docker_registry_image_name' => 'coolify/poc$(touch /tmp/pwned)',
|
|
'docker_registry_image_tag' => 'latest',
|
|
]);
|
|
$deploymentQueue = new ApplicationDeploymentQueue([
|
|
'docker_registry_image_tag' => null,
|
|
]);
|
|
|
|
$jobReflection = new ReflectionClass($job);
|
|
foreach ([
|
|
'application' => $application,
|
|
'application_deployment_queue' => $deploymentQueue,
|
|
'dockerImagePreviewTag' => null,
|
|
] as $property => $value) {
|
|
$reflectionProperty = $jobReflection->getProperty($property);
|
|
$reflectionProperty->setValue($job, $value);
|
|
}
|
|
|
|
$method = $jobReflection->getMethod('validateDockerRegistryImageConfiguration');
|
|
|
|
expect(fn () => $method->invoke($job))->toThrow(DeploymentException::class);
|
|
});
|