fix(applications): normalize nested base64 custom labels

This commit is contained in:
Andras Bacsai
2026-08-23 14:53:01 +02:00
parent 379abb2526
commit e49b0a2ac8
5 changed files with 82 additions and 10 deletions
@@ -4456,8 +4456,8 @@ class ApplicationsController extends Controller
],
], 422);
}
$customLabels = base64_decode($request->custom_labels);
if (mb_detect_encoding($customLabels, 'UTF-8', true) === false) {
$customLabels = decodeBase64EncodedLabels($request->custom_labels);
if ($customLabels === null) {
return response()->json([
'message' => 'Validation failed.',
'errors' => [
@@ -4465,6 +4465,7 @@ class ApplicationsController extends Controller
],
], 422);
}
$request->offsetSet('custom_labels', base64_encode($customLabels));
}
if ($request->has('domains') && $server->isProxyShouldRun()) {
$uuid = $request->uuid;
+8 -8
View File
@@ -2243,18 +2243,18 @@ class Application extends BaseModel
public function parseContainerLabels(?ApplicationPreview $preview = null)
{
$customLabels = data_get($this, 'custom_labels');
if (! $customLabels) {
$storedLabels = data_get($this, 'custom_labels');
if (! $storedLabels) {
return;
}
if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
$this->custom_labels = str($customLabels)->replace(',', "\n");
$this->custom_labels = base64_encode($customLabels);
}
$customLabels = base64_decode($this->custom_labels);
if (mb_detect_encoding($customLabels, 'UTF-8', true) === false) {
$customLabels = decodeBase64EncodedLabels($storedLabels);
if ($customLabels === null && ! isBase64Encoded($storedLabels)) {
$customLabels = str($storedLabels)->replace(',', "\n")->value();
} elseif ($customLabels === null) {
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
}
$this->custom_labels = base64_encode($customLabels);
$this->save();
+26
View File
@@ -2414,6 +2414,32 @@ function isBase64Encoded($strValue)
{
return base64_encode(base64_decode($strValue, true)) === $strValue;
}
function decodeBase64EncodedLabels(string $value): ?string
{
if (! isBase64Encoded($value)) {
return null;
}
$decoded = base64_decode($value, true);
$labels = $decoded;
while ($decoded !== '' && isBase64Encoded($decoded)) {
$decoded = base64_decode($decoded, true);
if (mb_detect_encoding($decoded, 'UTF-8', true) !== false) {
$lines = preg_split('/\r\n|\n|\r/', $decoded);
$containsOnlyLabels = collect($lines)
->filter(fn (string $line) => $line !== '')
->every(fn (string $line) => str_contains($line, '=') && ! str_starts_with($line, '='));
if ($containsOnlyLabels) {
$labels = $decoded;
}
}
}
return mb_detect_encoding($labels, 'UTF-8', true) === false ? null : $labels;
}
function customApiValidator(Collection|array $item, array $rules, array $messages = [])
{
if (is_array($item)) {
@@ -185,6 +185,26 @@ test('http basic auth updates preserve user-managed labels', function () {
expect(base64_decode($this->application->fresh()->custom_labels))->toBe('sentinel-label=true');
});
test('nested base64 custom labels are stored in canonical form', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.rule=Host(`example.com`)";
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
'custom_labels' => base64_encode(base64_encode($labels)),
])
->assertOk();
expect($this->application->fresh()->custom_labels)->toBe(base64_encode($labels));
});
test('deployment parsing repairs historically nested custom labels', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.rule=Host(`example.com`)";
$this->application->update(['custom_labels' => base64_encode(base64_encode($labels))]);
expect($this->application->parseContainerLabels())->toBe($labels)
->and($this->application->fresh()->custom_labels)->toBe(base64_encode($labels));
});
test('rejects invalid boolean application settings', function () {
$this->withHeaders(applicationSettingsApiHeaders($this->bearerToken))
->patchJson("/api/v1/applications/{$this->application->uuid}", [
+25
View File
@@ -0,0 +1,25 @@
<?php
it('decodes nested base64 container labels to label text', function () {
$labels = "traefik.enable=true\ntraefik.http.routers.web.rule=Host(`example.com`)";
expect(decodeBase64EncodedLabels(base64_encode(base64_encode($labels))))->toBe($labels);
});
it('decodes single encoded container labels without changing them', function () {
$labels = "traefik.enable=true\ncom.example.version=1";
expect(decodeBase64EncodedLabels(base64_encode($labels)))->toBe($labels);
});
it('rejects values that are not valid base64', function () {
expect(decodeBase64EncodedLabels('traefik.enable=true'))->toBeNull();
});
it('handles an empty encoded value', function () {
expect(decodeBase64EncodedLabels(''))->toBe('');
});
it('does not decode label text that happens to be valid base64', function () {
expect(decodeBase64EncodedLabels(base64_encode('foo=')))->toBe('foo=');
});