Skip to content

Commit f3ca349

Browse files
authored
fix: accept underscores in domain hostnames for API URL validation (#10663)
2 parents d01e3a9 + b50839d commit f3ca349

9 files changed

Lines changed: 99 additions & 11 deletions

File tree

app/Http/Controllers/Api/ApplicationsController.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1091,7 +1091,7 @@ private function create_application(Request $request, $type)
10911091

10921092
$errors = [];
10931093
$urls = $urls->map(function ($url) use (&$errors) {
1094-
if (! filter_var($url, FILTER_VALIDATE_URL)) {
1094+
if (! isValidDomainUrl($url)) {
10951095
$errors[] = "Invalid URL: {$url}";
10961096

10971097
return $url;
@@ -1332,7 +1332,7 @@ private function create_application(Request $request, $type)
13321332

13331333
$errors = [];
13341334
$urls = $urls->map(function ($url) use (&$errors) {
1335-
if (! filter_var($url, FILTER_VALIDATE_URL)) {
1335+
if (! isValidDomainUrl($url)) {
13361336
$errors[] = "Invalid URL: {$url}";
13371337

13381338
return $url;
@@ -1545,7 +1545,7 @@ private function create_application(Request $request, $type)
15451545

15461546
$errors = [];
15471547
$urls = $urls->map(function ($url) use (&$errors) {
1548-
if (! filter_var($url, FILTER_VALIDATE_URL)) {
1548+
if (! isValidDomainUrl($url)) {
15491549
$errors[] = "Invalid URL: {$url}";
15501550

15511551
return $url;
@@ -2551,7 +2551,7 @@ public function update_by_uuid(Request $request)
25512551

25522552
$errors = [];
25532553
$urls = $urls->map(function ($url) use (&$errors) {
2554-
if (! filter_var($url, FILTER_VALIDATE_URL)) {
2554+
if (! isValidDomainUrl($url)) {
25552555
$errors[] = "Invalid URL: {$url}";
25562556

25572557
return $url;

app/Support/ValidationPatterns.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ public static function validateApplicationDomains(mixed $value): array
557557
continue;
558558
}
559559

560-
if (! filter_var($url, FILTER_VALIDATE_URL)) {
560+
if (! isValidDomainUrl($url)) {
561561
$errors[] = "Invalid URL: {$url}";
562562

563563
continue;

bootstrap/helpers/domains.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,54 @@
44
use App\Models\ServiceApplication;
55
use Illuminate\Support\Collection;
66

7+
function isValidDomainUrl(string $url): bool
8+
{
9+
$components = parse_url($url);
10+
11+
if ($components === false) {
12+
return false;
13+
}
14+
15+
$scheme = $components['scheme'] ?? '';
16+
$host = $components['host'] ?? '';
17+
18+
if (! in_array(strtolower($scheme), ['http', 'https'], true) || $host === '') {
19+
return false;
20+
}
21+
22+
$urlToValidate = $scheme.'://';
23+
24+
if (isset($components['user'])) {
25+
$urlToValidate .= $components['user'];
26+
27+
if (isset($components['pass'])) {
28+
$urlToValidate .= ':'.$components['pass'];
29+
}
30+
31+
$urlToValidate .= '@';
32+
}
33+
34+
$urlToValidate .= str_replace('_', '-', $host);
35+
36+
if (isset($components['port'])) {
37+
$urlToValidate .= ':'.$components['port'];
38+
}
39+
40+
if (isset($components['path'])) {
41+
$urlToValidate .= $components['path'];
42+
}
43+
44+
if (isset($components['query'])) {
45+
$urlToValidate .= '?'.$components['query'];
46+
}
47+
48+
if (isset($components['fragment'])) {
49+
$urlToValidate .= '#'.$components['fragment'];
50+
}
51+
52+
return filter_var($urlToValidate, FILTER_VALIDATE_URL) !== false;
53+
}
54+
755
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
856
{
957
$conflicts = [];

openapi.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2553,6 +2553,10 @@
25532553
"is_preserve_repository_enabled": {
25542554
"type": "boolean",
25552555
"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."
2556+
},
2557+
"include_source_commit_in_build": {
2558+
"type": "boolean",
2559+
"description": "Include source commit information in the build. Default is false."
25562560
}
25572561
},
25582562
"type": "object"

openapi.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,9 @@ paths:
16631663
is_preserve_repository_enabled:
16641664
type: boolean
16651665
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.'
1666+
include_source_commit_in_build:
1667+
type: boolean
1668+
description: 'Include source commit information in the build. Default is false.'
16661669
type: object
16671670
responses:
16681671
'200':

templates/service-templates-latest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@
803803
"category": "backend",
804804
"logo": "svgs/convex.svg",
805805
"minversion": "0.0.0",
806-
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
806+
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
807807
"port": "6791"
808808
},
809809
"cryptgeon": {
@@ -1779,7 +1779,7 @@
17791779
"category": "devtools",
17801780
"logo": "svgs/gitea.svg",
17811781
"minversion": "0.0.0",
1782-
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
1782+
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
17831783
},
17841784
"gitea-with-mariadb": {
17851785
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
@@ -2361,7 +2361,7 @@
23612361
"category": "automation",
23622362
"logo": "svgs/inngest.png",
23632363
"minversion": "0.0.0",
2364-
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
2364+
"template_last_updated_at": null,
23652365
"port": "8288"
23662366
},
23672367
"invoice-ninja": {

templates/service-templates.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -803,7 +803,7 @@
803803
"category": "backend",
804804
"logo": "svgs/convex.svg",
805805
"minversion": "0.0.0",
806-
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
806+
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
807807
"port": "6791"
808808
},
809809
"cryptgeon": {
@@ -1779,7 +1779,7 @@
17791779
"category": "devtools",
17801780
"logo": "svgs/gitea.svg",
17811781
"minversion": "0.0.0",
1782-
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
1782+
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
17831783
},
17841784
"gitea-with-mariadb": {
17851785
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
@@ -2361,7 +2361,7 @@
23612361
"category": "automation",
23622362
"logo": "svgs/inngest.png",
23632363
"minversion": "0.0.0",
2364-
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
2364+
"template_last_updated_at": null,
23652365
"port": "8288"
23662366
},
23672367
"invoice-ninja": {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
it('accepts hostnames containing underscores', function () {
4+
// Regression: PHP's FILTER_VALIDATE_URL rejects underscores in the host,
5+
// which blocked valid service domains (e.g. Docker service naming) from
6+
// being saved and getting Let's Encrypt certificates. See issue #10597.
7+
expect(isValidDomainUrl('https://myapp_service.example.com'))->toBeTrue();
8+
expect(isValidDomainUrl('http://my_app.example.com'))->toBeTrue();
9+
expect(isValidDomainUrl('https://a_b_c.example.com/path'))->toBeTrue();
10+
});
11+
12+
it('accepts ordinary domains and URLs', function () {
13+
expect(isValidDomainUrl('https://example.com'))->toBeTrue();
14+
expect(isValidDomainUrl('http://sub.example.com:8080/path?q=1'))->toBeTrue();
15+
expect(isValidDomainUrl('https://example.com/a_b'))->toBeTrue();
16+
});
17+
18+
it('rejects strings that are not valid URLs', function () {
19+
expect(isValidDomainUrl('not a url'))->toBeFalse();
20+
expect(isValidDomainUrl('example.com'))->toBeFalse();
21+
expect(isValidDomainUrl('ht_tp://example.com'))->toBeFalse();
22+
expect(isValidDomainUrl(''))->toBeFalse();
23+
});
24+
25+
it('rejects URLs that do not use HTTP or HTTPS schemes', function () {
26+
expect(isValidDomainUrl('ftp://example.com'))->toBeFalse();
27+
expect(isValidDomainUrl('javascript://example.com'))->toBeFalse();
28+
expect(isValidDomainUrl('data://example.com'))->toBeFalse();
29+
});

tests/Unit/ValidationPatternsTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,7 @@
187187
expect(ValidationPatterns::normalizeApplicationDomains($domains))
188188
->toBe('https://example.com/MixedCase/Path?Token=ABC#Fragment,http://sub.example.com/Api/V1');
189189
});
190+
191+
it('validates application domains with underscores in the hostname', function () {
192+
expect(ValidationPatterns::validateApplicationDomains('https://myapp_service.example.com'))->toBeEmpty();
193+
});

0 commit comments

Comments
 (0)