Skip to content

Commit 8b7e9da

Browse files
author
Daniel Neto
committed
fix: Enhance security in url_get_contents and wget functions to prevent SSRF attacks by disabling automatic HTTP redirects
GHSA-f359-r3pv-2phf
1 parent aa2c46a commit 8b7e9da

File tree

2 files changed

+74
-8
lines changed

2 files changed

+74
-8
lines changed

objects/functions.php

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1963,7 +1963,25 @@ function url_get_contents($url, $ctx = "", $timeout = 0, $debug = false, $mantai
19631963
}
19641964
if (empty($ctx)) {
19651965
$opts = [
1966-
'http' => ['header' => "User-Agent: {$agent}\r\n"],
1966+
'http' => [
1967+
'header' => "User-Agent: {$agent}\r\n",
1968+
// SECURITY: Disable PHP's automatic HTTP redirect following.
1969+
//
1970+
// The problem this solves (called "SSRF via open redirect"):
1971+
// Before making a request, we call isSSRFSafeURL() to confirm the
1972+
// destination is not an internal/private server (e.g. 192.168.x.x
1973+
// or 169.254.169.254, the AWS cloud metadata endpoint).
1974+
// BUT if PHP silently follows redirects on its own, an attacker can
1975+
// point us at a public URL like https://attacker.com/redir that then
1976+
// redirects to http://169.254.169.254/secrets -- bypassing our check
1977+
// completely, because we only verified the *first* URL.
1978+
//
1979+
// The fix:
1980+
// Set follow_location = 0 so PHP stops at every redirect and hands
1981+
// control back to us. We then re-check each redirect target ourselves
1982+
// with isSSRFSafeURL() before deciding whether to follow it.
1983+
'follow_location' => 0,
1984+
],
19671985
'ssl' => [
19681986
'verify_peer' => false,
19691987
'verify_peer_name' => false,
@@ -1986,15 +2004,51 @@ function url_get_contents($url, $ctx = "", $timeout = 0, $debug = false, $mantai
19862004
_error_log("url_get_contents: allow_url_fopen {$url}");
19872005
}
19882006
try {
1989-
if ($debug) {
1990-
$tmp = file_get_contents($url, false, $context);
1991-
} else {
1992-
$tmp = @file_get_contents($url, false, $context);
2007+
$tmp = false;
2008+
$currentUrl = $url;
2009+
// SECURITY: Manual redirect loop -- we follow up to 5 redirects ourselves
2010+
// so we can run isSSRFSafeURL() on every new destination URL.
2011+
// Without this loop, PHP would never follow any redirect at all (because
2012+
// we disabled follow_location above), breaking legitimate HTTP->HTTPS
2013+
// upgrades. This gives us both safety and correct behavior.
2014+
for ($redirectCount = 0; $redirectCount <= 5; $redirectCount++) {
2015+
$fetched = $debug
2016+
? file_get_contents($currentUrl, false, $context)
2017+
: @file_get_contents($currentUrl, false, $context);
2018+
if ($fetched === false) {
2019+
break;
2020+
}
2021+
// $http_response_header is a PHP magic variable automatically populated
2022+
// by file_get_contents() with the raw HTTP response headers.
2023+
// We check whether the server responded with a 3xx redirect status code.
2024+
$redirectTarget = null;
2025+
if (!empty($http_response_header) && preg_match('/^HTTP\/\S+\s+3\d\d\s/i', $http_response_header[0])) {
2026+
foreach ($http_response_header as $h) {
2027+
if (preg_match('/^Location:\s*(.+)$/i', $h, $m)) {
2028+
$redirectTarget = trim($m[1]);
2029+
break;
2030+
}
2031+
}
2032+
}
2033+
if ($redirectTarget) {
2034+
// SECURITY: Re-validate the redirect destination before following it.
2035+
// This is the core of the fix -- every redirect hop is checked.
2036+
// If the target is an internal/private/reserved IP, we stop
2037+
// immediately and return false instead of leaking data.
2038+
if (!isSSRFSafeURL($redirectTarget)) {
2039+
_error_log("url_get_contents: blocked unsafe redirect from {$currentUrl} to {$redirectTarget}");
2040+
return false;
2041+
}
2042+
$currentUrl = $redirectTarget;
2043+
continue;
2044+
}
2045+
// No redirect -- this is the final response body we wanted.
2046+
$tmp = $fetched;
2047+
break;
19932048
}
19942049
if ($tmp !== false) {
19952050
$response = remove_utf8_bom($tmp);
19962051
if ($debug) {
1997-
//_error_log("url_get_contents: SUCCESS file_get_contents($url) {$response}");
19982052
_error_log("url_get_contents: SUCCESS file_get_contents($url)");
19992053
}
20002054
return $response;

objects/functionsExec.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,14 @@ function wget($url, $filename, $debug = false)
7878
}
7979
wgetLock($url);
8080
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
81-
$content = @file_get_contents($url);
81+
// SECURITY: Use a stream context with follow_location=0 so that file_get_contents
82+
// does NOT follow redirects automatically. Without this, an attacker could set up
83+
// a redirect from a trusted URL to an internal address (e.g. 192.168.x.x or the
84+
// cloud metadata endpoint 169.254.169.254) and bypass our SSRF checks.
85+
// On Windows we use file_get_contents as a last resort (wget is not available),
86+
// so we must disable redirects here too.
87+
$noRedirectCtx = stream_context_create(['http' => ['follow_location' => 0]]);
88+
$content = @file_get_contents($url, false, $noRedirectCtx);
8289
if (!empty($content) && file_put_contents($filename, $content) > 100) {
8390
wgetRemoveLock($url);
8491
return true;
@@ -89,7 +96,12 @@ function wget($url, $filename, $debug = false)
8996

9097
$filename = escapeshellarg($filename);
9198
$url = escapeshellarg($url);
92-
$cmd = "wget --tries=1 {$url} -O {$filename} --no-check-certificate";
99+
// SECURITY: --max-redirect=0 tells wget to NOT follow any HTTP redirects.
100+
// By default wget follows up to 20 redirects. An attacker that can make us
101+
// fetch a URL they control could redirect us to an internal server address,
102+
// bypassing the isSSRFSafeURL() check that was only done on the original URL.
103+
// With --max-redirect=0, wget fetches exactly the URL we give it and stops.
104+
$cmd = "wget --tries=1 --max-redirect=0 {$url} -O {$filename} --no-check-certificate";
93105
if ($debug) {
94106
_error_log("wget Start ({$cmd}) ");
95107
}

0 commit comments

Comments
 (0)