Skip to content

Commit 0d4fe62

Browse files
committed
Add local domains support. Fixes #168. Fixes #446
1 parent 3046feb commit 0d4fe62

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

app/Services/SanitizeService.php

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ public function isLocalObject($url): bool
234234
* - Blocks localhost/private/reserved IPs (IPv4/IPv6)
235235
* - Optionally verifies DNS and blocks records resolving to private/reserved IPs
236236
* - Normalizes IDNs to ASCII (punycode) when possible
237+
* - Allows configured local domains (same-server instances)
237238
*
238239
* @param string|array $input
239240
* @return string|false Normalized safe URL or false
@@ -273,8 +274,34 @@ public function url($input, bool $verifyDns = false, bool $bypassAppHost = true)
273274
}
274275

275276
$host = strtolower($parts['host']);
276-
if ($bypassAppHost && $host === $this->localDomain()) {
277-
return $input;
277+
278+
$isTrustedLocal = false;
279+
if ($bypassAppHost) {
280+
$trustedDomains = $this->getTrustedLocalDomains();
281+
foreach ($trustedDomains as $trustedDomain) {
282+
if ($host === $trustedDomain) {
283+
$isTrustedLocal = true;
284+
break;
285+
}
286+
}
287+
}
288+
289+
if ($isTrustedLocal) {
290+
$path = $parts['path'] ?? '';
291+
if ($path !== '' && strpos($path, '\\') !== false) {
292+
return false;
293+
}
294+
295+
$authority = (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) ? '['.$host.']' : $host;
296+
$query = isset($parts['query']) ? '?'.$parts['query'] : '';
297+
$fragment = isset($parts['fragment']) ? '#'.$parts['fragment'] : '';
298+
$normalized = 'https://'.$authority.$path.$query.$fragment;
299+
300+
if (! filter_var($normalized, FILTER_VALIDATE_URL)) {
301+
return false;
302+
}
303+
304+
return $normalized;
278305
}
279306

280307
$isIpv4 = filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false;
@@ -325,6 +352,23 @@ public function url($input, bool $verifyDns = false, bool $bypassAppHost = true)
325352
return $normalized;
326353
}
327354

355+
/**
356+
* Get list of trusted local domains (same-server instances)
357+
*/
358+
private function getTrustedLocalDomains(): array
359+
{
360+
$domains = [$this->localDomain()];
361+
362+
$configDomains = config('loops.local_domains');
363+
if ($configDomains) {
364+
$additional = array_map('trim', explode(',', $configDomains));
365+
$additional = array_filter($additional, fn ($d) => $d !== '');
366+
$domains = array_merge($domains, $additional);
367+
}
368+
369+
return array_unique(array_map('strtolower', $domains));
370+
}
371+
328372
private function idnToAscii(string $host)
329373
{
330374
if (preg_match('/[^\x20-\x7E]/', $host)) {

config/loops.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,16 @@
8585
'enabled' => env('LOOPS_HEALTH_ENDPOINT_ENABLED', false),
8686
'secret' => env('LOOPS_HEALTH_ENDPOINT_SECRET'),
8787
],
88+
89+
/*
90+
|--------------------------------------------------------------------------
91+
| Local Domains (Same-Server Instances)
92+
|--------------------------------------------------------------------------
93+
|
94+
| Comma-separated list of domains hosted on the same server that should
95+
| be allowed to federate even if they resolve to localhost/private IPs.
96+
| Useful for multi-instance setups on a single server.
97+
|
98+
*/
99+
'local_domains' => env('LOOPS_LOCAL_DOMAINS', ''),
88100
];

0 commit comments

Comments
 (0)