@@ -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 )) {
0 commit comments