@@ -4030,8 +4030,15 @@ function get_proxied_url($url)
40304030 $ parsedurl = parse_iri ($ url );
40314031
40324032 // Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
4033- // Don't proxy localhost or IP addresses, either
4034- if (empty ($ parsedurl ['scheme ' ]) || empty ($ parsedurl ['host ' ]) || empty ($ parsedurl ['path ' ]) || $ parsedurl ['scheme ' ] === 'https ' || $ parsedurl ['host ' ] === 'localhost ' || filter_var ($ parsedurl ['host ' ], FILTER_VALIDATE_IP ) !== false )
4033+ if (empty ($ parsedurl ['scheme ' ]) || empty ($ parsedurl ['host ' ]) || empty ($ parsedurl ['path ' ]) || $ parsedurl ['scheme ' ] === 'https ' )
4034+ return $ url ;
4035+
4036+ // Don't proxy URLs whose hosts are private or reserved IP addresses
4037+ if (filter_var ($ parsedurl ['host ' ], FILTER_VALIDATE_IP ) !== false && filter_var ($ parsedurl ['host ' ], FILTER_VALIDATE_IP , FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE ) === false )
4038+ return $ url ;
4039+
4040+ // Don't proxy URLs with domains that aren't part of public DNS
4041+ if (preg_match ('/\b(? ' . '>example|local(? ' . '>host)?|onion|test|alt|in(? ' . '>ternal|valid))$/ ' , $ parsedurl ['host ' ]))
40354042 return $ url ;
40364043
40374044 // We don't need to proxy our own resources
@@ -6117,6 +6124,15 @@ function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection
61176124 global $ webmaster_email , $ sourcedir , $ txt ;
61186125 static $ keep_alive_dom = null , $ keep_alive_fp = null ;
61196126
6127+ // SSRF guard: refuse loopback/private/link-local/reserved targets and
6128+ // non-fetchable schemes before any connection is attempted.
6129+ if (!is_fetch_safe ($ url ))
6130+ {
6131+ loadLanguage ('Errors ' );
6132+ trigger_error ($ txt ['fetch_web_data_bad_url ' ], E_USER_NOTICE );
6133+ return false ;
6134+ }
6135+
61206136 preg_match ('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~ ' , iri_to_url ($ url ), $ match );
61216137
61226138 // No scheme? No data for you!
@@ -6285,6 +6301,64 @@ function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection
62856301 return $ data ;
62866302}
62876303
6304+ /**
6305+ * Decides whether a URL is safe to fetch from the server.
6306+ *
6307+ * Rejects URLs whose scheme is not in the fetchable set, and URLs whose
6308+ * host resolves (or is) a non-global IP address: loopback, private,
6309+ * link-local (incl. 169.254.0.0/16 cloud metadata), or other reserved
6310+ * ranges. This is the single chokepoint that prevents the avatar, proxy,
6311+ * get_mime_type, and task fetchers from being used as SSRF primitives. It is
6312+ * also re-applied to each redirect target by the fetchers.
6313+ *
6314+ * @param string $url The URL to check.
6315+ * @return bool True if the URL is safe to fetch, false otherwise.
6316+ */
6317+ function is_fetch_safe ($ url )
6318+ {
6319+ static $ safe_hosts = array ();
6320+
6321+ $ parsedurl = parse_iri (iri_to_url ($ url ));
6322+
6323+ if (empty ($ parsedurl ['scheme ' ]) || !in_array ($ parsedurl ['scheme ' ], array ('http ' , 'https ' , 'ftp ' , 'ftps ' )) || empty ($ parsedurl ['host ' ]) || preg_match ('/\b(? ' . '>example|local(? ' . '>host)?|onion|test|alt|in(? ' . '>ternal|valid))$/ ' , $ parsedurl ['host ' ]))
6324+ return false ;
6325+
6326+ // Avoid unnecessary repetition.
6327+ if (isset ($ safe_hosts [$ parsedurl ['host ' ]]))
6328+ return $ safe_hosts [$ parsedurl ['host ' ]];
6329+
6330+ // Resolve the host to its address(es). A literal IP resolves to itself.
6331+ $ ips = array ();
6332+
6333+ if (filter_var ($ parsedurl ['host ' ], FILTER_VALIDATE_IP ) !== false )
6334+ {
6335+ $ ips [] = $ parsedurl ['host ' ];
6336+ }
6337+ else
6338+ {
6339+ $ records = @dns_get_record ($ parsedurl ['host ' ], DNS_A | DNS_AAAA );
6340+
6341+ foreach ((array ) $ records as $ record )
6342+ {
6343+ if (!empty ($ record ['ip ' ]))
6344+ $ ips [] = $ record ['ip ' ];
6345+
6346+ if (!empty ($ record ['ipv6 ' ]))
6347+ $ ips [] = $ record ['ipv6 ' ];
6348+ }
6349+ }
6350+
6351+ $ safe_hosts [$ parsedurl ['host ' ]] = !empty ($ ips ) && $ ips === array_filter (
6352+ $ ips ,
6353+ function ($ ip )
6354+ {
6355+ return filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_NO_RES_RANGE | FILTER_FLAG_NO_PRIV_RANGE );
6356+ }
6357+ );
6358+
6359+ return $ safe_hosts [$ parsedurl ['host ' ]];
6360+ }
6361+
62886362/**
62896363 * Attempts to determine the MIME type of some data or a file.
62906364 *
0 commit comments