2121use Illuminate \Validation \ValidationException ;
2222use Psr \Http \Message \ResponseInterface ;
2323use Psr \Http \Message \StreamInterface ;
24+ use Illuminate \Http \Response ;
2425use enshrined \svgSanitize \Sanitizer ;
2526
2627class ItemController extends Controller
@@ -490,25 +491,48 @@ public function testConfig(Request $request)
490491 */
491492 public function execute ($ url , array $ attrs = [], $ overridevars = false ): ?ResponseInterface
492493 {
493- $ vars = ($ overridevars !== false ) ?
494- $ overridevars : [
495- 'http_errors ' => false ,
496- 'timeout ' => 15 ,
497- 'connect_timeout ' => 15 ,
498- 'verify ' => false ,
499- ];
494+ // Default Guzzle client configuration
495+ $ clientOptions = [
496+ 'http_errors ' => false ,
497+ 'timeout ' => 15 ,
498+ 'connect_timeout ' => 15 ,
499+ 'verify ' => false , // In production, set this to `true` and manage certs.
500+ ];
501+
502+ // If the user provided overrides, use them.
503+ if ($ overridevars !== false ) {
504+ $ clientOptions = $ overridevars ;
505+ }
506+
507+ // Resolve the hostname to an IP address
508+ $ host = parse_url ($ url , PHP_URL_HOST );
509+ $ ip = gethostbyname ($ host );
500510
501- $ client = new Client ($ vars );
511+ // Check if the IP is private or reserved
512+ $ allowInternalIps = env ('ALLOW_INTERNAL_REQUESTS ' , false );
513+ if (!$ allowInternalIps && filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
514+ Log::warning ('Blocked access to private or reserved IPs. ' , ['ip ' => $ ip , 'host ' => $ host ]);
515+ abort (Response::HTTP_FORBIDDEN , 'Access to private or reserved IPs is not allowed. ' );
516+ }
517+
518+ // Force Guzzle to use the resolved IP address
519+ $ clientOptions ['curl ' ][CURLOPT_RESOLVE ] = ["{$ host }:80: {$ ip }" , "{$ host }:443: {$ ip }" ];
502520
521+ $ client = new Client ($ clientOptions );
503522 $ method = 'GET ' ;
504523
505524 try {
506525 return $ client ->request ($ method , $ url , $ attrs );
507526 } catch (ConnectException $ e ) {
508- Log::error ('Connection refused ' );
509- Log::debug ($ e ->getMessage ());
527+ Log::warning ('SSRF Attempt Blocked: Connection to a private IP was prevented. ' , [
528+ 'url ' => $ url ,
529+ 'error ' => $ e ->getMessage ()
530+ ]);
531+ return null ;
510532 } catch (ServerException $ e ) {
511533 Log::debug ($ e ->getMessage ());
534+ } catch (\Exception $ e ) {
535+ Log::error ('General error: ' . $ e ->getMessage ());
512536 }
513537
514538 return null ;
@@ -520,10 +544,22 @@ public function execute($url, array $attrs = [], $overridevars = false): ?Respon
520544 */
521545 public function websitelookup ($ url ): StreamInterface
522546 {
523- $ url = base64_decode ($ url );
524- $ data = $ this ->execute ($ url );
547+ $ decodedUrl = base64_decode ($ url );
548+
549+ // Validate the URL format.
550+ if (filter_var ($ decodedUrl , FILTER_VALIDATE_URL ) === false ) {
551+ abort (Response::HTTP_BAD_REQUEST , 'Invalid URL format provided. ' );
552+ }
553+
554+ $ response = $ this ->execute ($ decodedUrl );
525555
526- return $ data ->getBody ();
556+ // If execute() returns null, it means the connection failed.
557+ // This can happen for many reasons, including our SSRF protection kicking in.
558+ if ($ response === null ) {
559+ abort (Response::HTTP_FORBIDDEN , 'Access to the requested resource is not allowed or the resource is unavailable. ' );
560+ }
561+
562+ return $ response ->getBody ();
527563 }
528564
529565 /**
0 commit comments