From 74b1077010f684282dffab267c5254afa648419c Mon Sep 17 00:00:00 2001 From: Osamaali313 <86572800+Osamaali313@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:34:43 +0300 Subject: [PATCH 1/2] fix: accept underscores in domain hostnames for API URL validation PHP's FILTER_VALIDATE_URL rejects underscores in the host, so domains like https://myapp_service.example.com were rejected by the API and never got a Let's Encrypt certificate. Add an isValidDomainUrl() helper that validates a copy with underscores replaced by hyphens, and route domain validation in the Applications and Services API controllers through it. Fixes #10597 --- .../Api/ApplicationsController.php | 12 +++++----- .../Controllers/Api/ServicesController.php | 2 +- bootstrap/helpers/domains.php | 10 +++++++++ tests/Unit/IsValidDomainUrlTest.php | 22 +++++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 tests/Unit/IsValidDomainUrlTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 5e5405a7a..b2f09e008 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1084,7 +1084,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -1325,7 +1325,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -1538,7 +1538,7 @@ private function create_application(Request $request, $type) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -2490,7 +2490,7 @@ public function update_by_uuid(Request $request) return null; } - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = 'Invalid URL: '.$url; return $url; @@ -2553,7 +2553,7 @@ public function update_by_uuid(Request $request) $errors = []; $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; @@ -3866,7 +3866,7 @@ private function validateDataApplications(Request $request, Server $server) return null; } - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = 'Invalid URL: '.$url; return str($url)->lower(); diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 11a23d46c..b0c6aa8a5 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -57,7 +57,7 @@ private function applyServiceUrls(Service $service, array $urlsArray, string $te }); $urls = $urls->map(function ($url) use (&$errors) { - if (! filter_var($url, FILTER_VALIDATE_URL)) { + if (! isValidDomainUrl($url)) { $errors[] = "Invalid URL: {$url}"; return $url; diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index ff77a78e2..1aa182510 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -4,6 +4,16 @@ use App\Models\ServiceApplication; use Illuminate\Support\Collection; +function isValidDomainUrl(string $url): bool +{ + // PHP's FILTER_VALIDATE_URL rejects underscores in the host, but they are + // accepted by browsers, Let's Encrypt, and common Docker service naming + // (e.g. https://myapp_service.example.com). Validate against a copy with + // underscores replaced by hyphens (a valid host character) so such domains + // are not wrongly rejected; URLs without underscores are unaffected. + return filter_var(str_replace('_', '-', $url), FILTER_VALIDATE_URL) !== false; +} + function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { $conflicts = []; diff --git a/tests/Unit/IsValidDomainUrlTest.php b/tests/Unit/IsValidDomainUrlTest.php new file mode 100644 index 000000000..940eaecea --- /dev/null +++ b/tests/Unit/IsValidDomainUrlTest.php @@ -0,0 +1,22 @@ +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(''))->toBeFalse(); +}); From bce871d9912b10ef2509241c7d0e119da0305f3e Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:20:26 +0200 Subject: [PATCH 2/2] fix(domains): reject non-HTTP URL schemes --- bootstrap/helpers/domains.php | 50 +++++++++++++++++++++++++---- tests/Unit/IsValidDomainUrlTest.php | 7 ++++ 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/bootstrap/helpers/domains.php b/bootstrap/helpers/domains.php index 1aa182510..f3c5359f7 100644 --- a/bootstrap/helpers/domains.php +++ b/bootstrap/helpers/domains.php @@ -6,12 +6,50 @@ function isValidDomainUrl(string $url): bool { - // PHP's FILTER_VALIDATE_URL rejects underscores in the host, but they are - // accepted by browsers, Let's Encrypt, and common Docker service naming - // (e.g. https://myapp_service.example.com). Validate against a copy with - // underscores replaced by hyphens (a valid host character) so such domains - // are not wrongly rejected; URLs without underscores are unaffected. - return filter_var(str_replace('_', '-', $url), FILTER_VALIDATE_URL) !== false; + $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) diff --git a/tests/Unit/IsValidDomainUrlTest.php b/tests/Unit/IsValidDomainUrlTest.php index 940eaecea..3f40646f7 100644 --- a/tests/Unit/IsValidDomainUrlTest.php +++ b/tests/Unit/IsValidDomainUrlTest.php @@ -18,5 +18,12 @@ 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(); +});