@@ -4100,44 +4100,24 @@ function isSSRFSafeURL($url)
41004100 return false ;
41014101 }
41024102
4103- // Block private IPv4 ranges
4104- // 10.0.0.0 - 10.255.255.255
4105- if (preg_match ('/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4106- _error_log ("isSSRFSafeURL: blocked private IP (10.x.x.x): {$ ip }" );
4103+ // Normalize IPv4-mapped IPv6 addresses (::ffff:x.x.x.x) to their plain IPv4 form.
4104+ // Without this, dotted-decimal private-range regexes and PHP's FILTER_FLAG_NO_PRIV_RANGE
4105+ // flag both miss the mapped address — the bypass vector reported in CVE-class SSRF findings.
4106+ if (preg_match ('/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i ' , $ ip , $ mapped )) {
4107+ _error_log ("isSSRFSafeURL: normalized IPv4-mapped IPv6 {$ ip } to {$ mapped [1 ]}" );
4108+ $ ip = $ mapped [1 ];
4109+ }
4110+
4111+ // Block all private and reserved IP ranges using PHP's built-in validation flags.
4112+ // FILTER_FLAG_NO_PRIV_RANGE rejects: RFC 1918 (10/8, 172.16/12, 192.168/16), fc00::/7
4113+ // FILTER_FLAG_NO_RES_RANGE rejects: 0/8, 127/8, 169.254/16, ::1, fe80::/10, and others
4114+ // This replaces the previous manual regex checks and the separate IPv6 block section.
4115+ if (!filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )) {
4116+ _error_log ("isSSRFSafeURL: blocked private/reserved IP: {$ ip }" );
41074117 return false ;
41084118 }
41094119
4110- // 172.16.0.0 - 172.31.255.255
4111- if (preg_match ('/^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4112- _error_log ("isSSRFSafeURL: blocked private IP (172.16-31.x.x): {$ ip }" );
4113- return false ;
4114- }
4115-
4116- // 192.168.0.0 - 192.168.255.255
4117- if (preg_match ('/^192\.168\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4118- _error_log ("isSSRFSafeURL: blocked private IP (192.168.x.x): {$ ip }" );
4119- return false ;
4120- }
4121-
4122- // 127.0.0.0 - 127.255.255.255 (loopback)
4123- if (preg_match ('/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4124- _error_log ("isSSRFSafeURL: blocked loopback IP: {$ ip }" );
4125- return false ;
4126- }
4127-
4128- // 169.254.0.0 - 169.254.255.255 (link-local, includes AWS/cloud metadata)
4129- if (preg_match ('/^169\.254\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4130- _error_log ("isSSRFSafeURL: blocked link-local/metadata IP: {$ ip }" );
4131- return false ;
4132- }
4133-
4134- // 0.0.0.0 - 0.255.255.255
4135- if (preg_match ('/^0\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ ' , $ ip )) {
4136- _error_log ("isSSRFSafeURL: blocked zero IP range: {$ ip }" );
4137- return false ;
4138- }
4139-
4140- // Block metadata endpoints (cloud providers)
4120+ // Block cloud metadata endpoints by hostname (defence-in-depth alongside the IP check)
41414121 $ metadataHosts = [
41424122 'metadata.google.internal ' ,
41434123 'metadata.goog ' ,
@@ -4150,25 +4130,6 @@ function isSSRFSafeURL($url)
41504130 }
41514131 }
41524132
4153- // Block IPv6 private/local addresses
4154- if (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 )) {
4155- // Check for IPv6 loopback
4156- if ($ ip === '::1 ' || $ ip === '0:0:0:0:0:0:0:1 ' ) {
4157- _error_log ("isSSRFSafeURL: blocked IPv6 loopback: {$ ip }" );
4158- return false ;
4159- }
4160- // Check for IPv6 link-local (fe80::/10)
4161- if (preg_match ('/^fe[89ab][0-9a-f]:/i ' , $ ip )) {
4162- _error_log ("isSSRFSafeURL: blocked IPv6 link-local: {$ ip }" );
4163- return false ;
4164- }
4165- // Check for IPv6 unique local (fc00::/7)
4166- if (preg_match ('/^f[cd][0-9a-f]{2}:/i ' , $ ip )) {
4167- _error_log ("isSSRFSafeURL: blocked IPv6 unique local: {$ ip }" );
4168- return false ;
4169- }
4170- }
4171-
41724133 return true ;
41734134}
41744135
0 commit comments