@@ -489,9 +489,37 @@ public function checkWebhooksHealth(): array
489489 $ call = 'hooks/list ' ;
490490 $ query = '' ; // no params
491491 $ checksum = sha1 ($ call . $ query . $ salt );
492- $ url = $ host . '/api/ ' . $ call . '?checksum= ' . $ checksum ;
492+ $ url = $ host . '/api/ ' . $ call . '?checksum= ' . $ checksum ;
493+
494+ // Fetch XML over HTTP(S) using cURL to avoid allowing the XML parser
495+ // to perform network requests or entity resolution (prevents XXE/SSRF).
496+ $ ch = curl_init ($ url );
497+ curl_setopt ($ ch , CURLOPT_RETURNTRANSFER , true );
498+ curl_setopt ($ ch , CURLOPT_TIMEOUT , 5 );
499+ curl_setopt ($ ch , CURLOPT_FOLLOWLOCATION , false );
500+ curl_setopt ($ ch , CURLOPT_SSL_VERIFYPEER , true );
501+ curl_setopt ($ ch , CURLOPT_SSL_VERIFYHOST , 2 );
502+ $ content = curl_exec ($ ch );
503+ $ curlErr = curl_errno ($ ch );
504+ curl_close ($ ch );
505+
506+ if ($ content === false || $ curlErr ) {
507+ return ['enabled ' => true , 'ok ' => false , 'reason ' => 'connection_failed ' ];
508+ }
509+
510+ // Disable external entity loader when available (PHP 8+ deprecates it,
511+ // so check for existence). Also use internal errors and LIBXML_NONET.
512+ $ prevEntityLoader = null ;
513+ if (function_exists ('libxml_disable_entity_loader ' )) {
514+ $ prevEntityLoader = libxml_disable_entity_loader (true );
515+ }
516+ libxml_use_internal_errors (true );
517+ $ xml = @simplexml_load_string ($ content , \SimpleXMLElement::class, LIBXML_NONET );
518+ if ($ prevEntityLoader !== null ) {
519+ libxml_disable_entity_loader ($ prevEntityLoader );
520+ }
521+ libxml_clear_errors ();
493522
494- $ xml = @simplexml_load_file ($ url );
495523 if ($ xml && (string )($ xml ->returncode ?? '' ) === 'SUCCESS ' ) {
496524 return ['enabled ' => true , 'ok ' => true ];
497525 }
0 commit comments