fix: accept underscores in domain hostnames for API URL validation (#10663)

This commit is contained in:
Andras Bacsai
2026-07-07 12:27:29 +02:00
committed by GitHub
9 changed files with 99 additions and 11 deletions
@@ -1091,7 +1091,7 @@ class ApplicationsController extends Controller
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";
return $url;
@@ -1332,7 +1332,7 @@ class ApplicationsController extends Controller
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";
return $url;
@@ -1545,7 +1545,7 @@ class ApplicationsController extends Controller
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";
return $url;
@@ -2551,7 +2551,7 @@ class ApplicationsController extends Controller
$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";
return $url;
+1 -1
View File
@@ -557,7 +557,7 @@ class ValidationPatterns
continue;
}
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";
continue;
+48
View File
@@ -4,6 +4,54 @@ use App\Models\Application;
use App\Models\ServiceApplication;
use Illuminate\Support\Collection;
function isValidDomainUrl(string $url): bool
{
$components = parse_url($url);
if ($components === false) {
return false;
}
$scheme = $components['scheme'] ?? '';
$host = $components['host'] ?? '';
if (! in_array(strtolower($scheme), ['http', 'https'], true) || $host === '') {
return false;
}
$urlToValidate = $scheme.'://';
if (isset($components['user'])) {
$urlToValidate .= $components['user'];
if (isset($components['pass'])) {
$urlToValidate .= ':'.$components['pass'];
}
$urlToValidate .= '@';
}
$urlToValidate .= str_replace('_', '-', $host);
if (isset($components['port'])) {
$urlToValidate .= ':'.$components['port'];
}
if (isset($components['path'])) {
$urlToValidate .= $components['path'];
}
if (isset($components['query'])) {
$urlToValidate .= '?'.$components['query'];
}
if (isset($components['fragment'])) {
$urlToValidate .= '#'.$components['fragment'];
}
return filter_var($urlToValidate, FILTER_VALIDATE_URL) !== false;
}
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
$conflicts = [];
+4
View File
@@ -2553,6 +2553,10 @@
"is_preserve_repository_enabled": {
"type": "boolean",
"description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
},
"include_source_commit_in_build": {
"type": "boolean",
"description": "Include source commit information in the build. Default is false."
}
},
"type": "object"
+3
View File
@@ -1663,6 +1663,9 @@ paths:
is_preserve_repository_enabled:
type: boolean
description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
include_source_commit_in_build:
type: boolean
description: 'Include source commit information in the build. Default is false.'
type: object
responses:
'200':
+3 -3
View File
@@ -803,7 +803,7 @@
"category": "backend",
"logo": "svgs/convex.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
"port": "6791"
},
"cryptgeon": {
@@ -1779,7 +1779,7 @@
"category": "devtools",
"logo": "svgs/gitea.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
},
"gitea-with-mariadb": {
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
@@ -2361,7 +2361,7 @@
"category": "automation",
"logo": "svgs/inngest.png",
"minversion": "0.0.0",
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
"template_last_updated_at": null,
"port": "8288"
},
"invoice-ninja": {
+3 -3
View File
@@ -803,7 +803,7 @@
"category": "backend",
"logo": "svgs/convex.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
"port": "6791"
},
"cryptgeon": {
@@ -1779,7 +1779,7 @@
"category": "devtools",
"logo": "svgs/gitea.svg",
"minversion": "0.0.0",
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
},
"gitea-with-mariadb": {
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
@@ -2361,7 +2361,7 @@
"category": "automation",
"logo": "svgs/inngest.png",
"minversion": "0.0.0",
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
"template_last_updated_at": null,
"port": "8288"
},
"invoice-ninja": {
+29
View File
@@ -0,0 +1,29 @@
<?php
it('accepts hostnames containing underscores', function () {
// Regression: PHP's FILTER_VALIDATE_URL rejects underscores in the host,
// which blocked valid service domains (e.g. Docker service naming) from
// being saved and getting Let's Encrypt certificates. See issue #10597.
expect(isValidDomainUrl('https://myapp_service.example.com'))->toBeTrue();
expect(isValidDomainUrl('http://my_app.example.com'))->toBeTrue();
expect(isValidDomainUrl('https://a_b_c.example.com/path'))->toBeTrue();
});
it('accepts ordinary domains and URLs', function () {
expect(isValidDomainUrl('https://example.com'))->toBeTrue();
expect(isValidDomainUrl('http://sub.example.com:8080/path?q=1'))->toBeTrue();
expect(isValidDomainUrl('https://example.com/a_b'))->toBeTrue();
});
it('rejects strings that are not valid URLs', function () {
expect(isValidDomainUrl('not a url'))->toBeFalse();
expect(isValidDomainUrl('example.com'))->toBeFalse();
expect(isValidDomainUrl('ht_tp://example.com'))->toBeFalse();
expect(isValidDomainUrl(''))->toBeFalse();
});
it('rejects URLs that do not use HTTP or HTTPS schemes', function () {
expect(isValidDomainUrl('ftp://example.com'))->toBeFalse();
expect(isValidDomainUrl('javascript://example.com'))->toBeFalse();
expect(isValidDomainUrl('data://example.com'))->toBeFalse();
});
+4
View File
@@ -187,3 +187,7 @@ it('normalizes application domain scheme and host without lowercasing path query
expect(ValidationPatterns::normalizeApplicationDomains($domains))
->toBe('https://example.com/MixedCase/Path?Token=ABC#Fragment,http://sub.example.com/Api/V1');
});
it('validates application domains with underscores in the hostname', function () {
expect(ValidationPatterns::validateApplicationDomains('https://myapp_service.example.com'))->toBeEmpty();
});