CVE-2026-43936
Description
e107 is a content management system (CMS). Prior to 2.3.4, you can access the local environment by specifying the URL of the local environment from "Image/File URL:" of "From a remote location" in "Media Manager" on the administrator screen. This vulnerability is fixed in 2.3.4.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
e107 CMS 2.3.3 and below allow authenticated admins to exploit a server-side request forgery (SSRF) in the Media Manager's remote file import, enabling local port scanning and potential information disclosure.
Vulnerability
In e107 CMS prior to version 2.3.4, the Media Manager's remote file import feature (accessible at /e107_admin/image.php?mode=main&action=import) allows an authenticated administrator to specify a URL in the "Image/File URL:" field. The application does not sufficiently validate the supplied URL, permitting requests to private IP ranges, loopback addresses, and other local network endpoints. This is a server-side request forgery (SSRF) vulnerability [1] [2].
Exploitation
An attacker with administrative access to the e107 CMS can trigger the vulnerability by: 1. Logging into the admin panel and navigating to "Media Upload/Import" (or directly accessing /e107_admin/image.php?mode=main&action=import). 2. In the "Image/File URL:" field of the "From a remote location" section, entering a URL pointing to a local or private IP address (e.g., http://localhost:80/test.png or http://169.254.169.254/). 3. Submitting the form. The server then attempts to fetch the resource from the provided URL, and the response (whether it errors or succeeds) reveals information about the target's availability. This allows an attacker to perform a port scan of the host's internal network or interact with local services [2].
Impact
Successful exploitation enables an authenticated administrator to conduct port scans against the host's local environment (e.g., localhost, private subnets, cloud metadata services like 169.254.169.254). This can lead to discovery of internal services and potential access to sensitive information (e.g., cloud provider metadata, credentials, or configuration endpoints) [2]. The vulnerability does not by itself result in remote code execution, but it provides reconnaissance and data exfiltration capabilities within the local network context.
Mitigation
The vulnerability is fixed in e107 CMS version 2.3.4, released on the same date as the advisory [2]. The fix, implemented in commit 5f98cc9f, adds an e_file::isUrlSafe() function that blocks private, loopback, and reserved IP ranges as well as non-HTTP(S) schemes. A second commit (40b2d111) canonicalizes IPv4-mapped IPv6 addresses (e.g., ::ffff:127.0.0.1) before the range check to prevent bypass attempts [1] [3]. Self-hosted deployments that legitimately need to fetch from private hosts can opt back in by defining e_REMOTE_FILE_ALLOW_PRIVATE to true in e107_config.php [2]. There is no known workaround for unpatched installations; upgrading to 2.3.4 is strongly recommended. The vulnerability is not currently listed on the CISA Known Exploited Vulnerabilities (KEV) catalog.
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
240b2d111fix(file): Canonicalize IPv4-mapped IPv6 before SSRF range check
1 file changed · +33 −1
e107_handlers/file_class.php+33 −1 modified@@ -597,7 +597,12 @@ public function isUrlSafe($url) foreach($ips as $ip) { - if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) + $canonical = $this->canonicalizeIp($ip); + if($canonical === false) + { + return false; + } + if(!filter_var($canonical, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return false; } @@ -607,6 +612,33 @@ public function isUrlSafe($url) } + /** + * Canonicalize an IP for range validation. IPv4-mapped IPv6 addresses + * (`::ffff:X.X.X.X`) are returned as their embedded IPv4 form so + * `FILTER_FLAG_NO_RES_RANGE` evaluates the IPv4 ranges; PHP's filter + * does not normalize the mapped form on its own, leaving + * `::ffff:127.0.0.1` reachable as a loopback evasion. + * + * @param string $ip + * @return string|false canonical IP, or false if the input is not an IP + */ + private function canonicalizeIp($ip) + { + $packed = @inet_pton($ip); + if($packed === false) + { + return false; + } + + if(strlen($packed) === 16 && substr($packed, 0, 10) === str_repeat("\x00", 10) && substr($packed, 10, 2) === "\xff\xff") + { + return inet_ntop(substr($packed, 12, 4)); + } + + return $ip; + } + + /** * Grab a remote file and save it in the /temp directory. requires CURL *
5f98cc9ffix(file): Block SSRF via private IPs in remote file fetching
2 files changed · +216 −0
e107_handlers/file_class.php+86 −0 modified@@ -539,6 +539,74 @@ public function getFileInfo($path_to_file, $imgcheck = true, $auto_fix_ext = tru } + /** + * Reject URLs that point at private/reserved IP ranges or non-HTTP(S) protocols. + * Define `e_REMOTE_FILE_ALLOW_PRIVATE` to bypass for legitimate intranet use. + * + * @param string $url + * @return bool + */ + public function isUrlSafe($url) + { + if(defined('e_REMOTE_FILE_ALLOW_PRIVATE') && e_REMOTE_FILE_ALLOW_PRIVATE === true) + { + return true; + } + + $parts = parse_url($url); + if(empty($parts['host'])) + { + return false; + } + + $scheme = isset($parts['scheme']) ? strtolower($parts['scheme']) : ''; + if($scheme !== 'http' && $scheme !== 'https') + { + return false; + } + + $host = $parts['host']; + if($host[0] === '[' && substr($host, -1) === ']') + { + $host = substr($host, 1, -1); + } + + $ips = array(); + if(filter_var($host, FILTER_VALIDATE_IP)) + { + $ips[] = $host; + } + else + { + $records = @dns_get_record($host, DNS_A | DNS_AAAA); + if(!is_array($records)) + { + return false; + } + foreach($records as $r) + { + if(!empty($r['ip'])) { $ips[] = $r['ip']; } + if(!empty($r['ipv6'])) { $ips[] = $r['ipv6']; } + } + } + + if(empty($ips)) + { + return false; + } + + foreach($ips as $ip) + { + if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) + { + return false; + } + } + + return true; + } + + /** * Grab a remote file and save it in the /temp directory. requires CURL * @@ -563,6 +631,13 @@ function getRemoteFile($remote_url, $local_file, $type = 'temp', $timeout = 40) return false; // May not be installed } + if(!$this->isUrlSafe($remote_url)) + { + $this->error = 'Refused to fetch URL with non-HTTP(S) scheme or private/reserved IP: ' . $remote_url; + error_log($this->error); + return false; + } + $path = ($type === 'media') ? e_MEDIA : e_TEMP; if($type === 'import') @@ -620,6 +695,11 @@ public function initCurl($address, $options = null) curl_setopt($cu, CURLOPT_REFERER, $referer); curl_setopt($cu, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($cu, CURLOPT_FOLLOWLOCATION, true); + if(defined('CURLOPT_PROTOCOLS') && defined('CURLPROTO_HTTP') && defined('CURLPROTO_HTTPS')) + { + curl_setopt($cu, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + curl_setopt($cu, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } curl_setopt($cu, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0"); curl_setopt($cu, CURLOPT_COOKIEFILE, e_SYSTEM . 'cookies.txt'); curl_setopt($cu, CURLOPT_COOKIEJAR, e_SYSTEM . 'cookies.txt'); @@ -698,6 +778,12 @@ public function getRemoteContent($address, $options = array()) $address = str_replace(array("\r", "\n", "\t", '&'), array('', '', '', '&'), $address); + if(!$this->isUrlSafe($address)) + { + $this->error = 'Refused to fetch URL with non-HTTP(S) scheme or private/reserved IP: ' . $address; + return false; + } + // ... and there shouldn't be unprintable characters in the URL anyway $requireCurl = false;
e107_tests/tests/unit/e_fileTest.php+130 −0 modified@@ -586,6 +586,136 @@ public function testUnzipGithubArchive() } } + /** + * Public addresses (real-world resolvable IPs and unbracketed/bracketed + * IPv6) should be allowed through `e_file::isUrlSafe()`. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeAcceptsPublicAddresses() + { + $this->assertTrue($this->fl->isUrlSafe('http://1.1.1.1/')); + $this->assertTrue($this->fl->isUrlSafe('https://8.8.8.8/')); + $this->assertTrue($this->fl->isUrlSafe('http://93.184.216.34/')); + $this->assertTrue($this->fl->isUrlSafe('http://[2606:4700:4700::1111]/')); + } + + /** + * RFC 1918 ranges (10/8, 172.16/12, 192.168/16) are the canonical + * SSRF-pivot targets and must be rejected. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsPrivateIPv4() + { + $this->assertFalse($this->fl->isUrlSafe('http://10.0.0.1/')); + $this->assertFalse($this->fl->isUrlSafe('http://10.255.255.255/')); + $this->assertFalse($this->fl->isUrlSafe('http://172.16.0.1/')); + $this->assertFalse($this->fl->isUrlSafe('http://172.31.255.255/')); + $this->assertFalse($this->fl->isUrlSafe('http://192.168.0.1/')); + $this->assertFalse($this->fl->isUrlSafe('http://192.168.255.255/')); + } + + /** + * Reserved/loopback/link-local/broadcast IPv4 ranges, including the + * AWS-style metadata endpoint at 169.254.169.254. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsReservedIPv4() + { + $this->assertFalse($this->fl->isUrlSafe('http://0.0.0.0/')); + $this->assertFalse($this->fl->isUrlSafe('http://127.0.0.1/')); + $this->assertFalse($this->fl->isUrlSafe('http://169.254.169.254/')); + $this->assertFalse($this->fl->isUrlSafe('http://255.255.255.255/')); + } + + /** + * IPv6 loopback, ULA, link-local, and IPv4-mapped IPv6 forms must be + * rejected. The IPv4-mapped case (`::ffff:127.0.0.1`) is a common + * filter-evasion vector that PHP's FILTER_FLAG_NO_RES_RANGE catches. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsReservedIPv6() + { + $this->assertFalse($this->fl->isUrlSafe('http://[::1]/')); + $this->assertFalse($this->fl->isUrlSafe('http://[fc00::1]/')); + $this->assertFalse($this->fl->isUrlSafe('http://[fd00::dead:beef]/')); + $this->assertFalse($this->fl->isUrlSafe('http://[fe80::1]/')); + $this->assertFalse($this->fl->isUrlSafe('http://[::ffff:127.0.0.1]/')); + } + + /** + * Anything other than http(s) must be refused, even if the host would + * otherwise pass the IP check. This stops file://, gopher://, dict://, + * etc. before they ever reach cURL. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsNonHttpSchemes() + { + $this->assertFalse($this->fl->isUrlSafe('file:///etc/passwd')); + $this->assertFalse($this->fl->isUrlSafe('gopher://1.1.1.1/')); + $this->assertFalse($this->fl->isUrlSafe('ftp://1.1.1.1/')); + $this->assertFalse($this->fl->isUrlSafe('dict://1.1.1.1/')); + $this->assertFalse($this->fl->isUrlSafe('javascript:alert(1)')); + } + + /** + * Empty, schemeless, hostless, or otherwise unparseable URLs should be + * rejected without making any network call. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsMalformedUrls() + { + $this->assertFalse($this->fl->isUrlSafe('')); + $this->assertFalse($this->fl->isUrlSafe('not-a-url')); + $this->assertFalse($this->fl->isUrlSafe('http:///path-only')); + $this->assertFalse($this->fl->isUrlSafe('//1.1.1.1/')); + } + + /** + * Decimal/hex/octal-encoded IPv4 forms (e.g. `2130706433` for + * 127.0.0.1) bypass FILTER_VALIDATE_IP because they aren't dotted + * quads. They then fail dns_get_record (no valid A/AAAA record), + * so the no-IPs-found branch rejects them. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testIsUrlSafeRejectsObfuscatedAddresses() + { + $this->assertFalse($this->fl->isUrlSafe('http://2130706433/')); + $this->assertFalse($this->fl->isUrlSafe('http://0x7f000001/')); + } + + /** + * `getRemoteFile()` should refuse SSRF candidates without making any + * cURL call and surface a meaningful error message. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testGetRemoteFileRejectsUnsafeUrl() + { + $result = $this->fl->getRemoteFile('http://127.0.0.1/foo.jpg', 'foo.jpg'); + $this->assertFalse($result); + $this->assertStringContainsString('private/reserved IP', $this->fl->getErrorMessage()); + } + + /** + * `getRemoteContent()` should refuse SSRF candidates without making + * any cURL call and surface a meaningful error message. + * + * Ref: GHSA-92fr-7h4f-22pp. + */ + public function testGetRemoteContentRejectsUnsafeUrl() + { + $result = $this->fl->getRemoteContent('http://10.0.0.1/'); + $this->assertFalse($result); + $this->assertStringContainsString('private/reserved IP', $this->fl->getErrorMessage()); + } + /* public function testGetRootFolder() {
Vulnerability mechanics
Root cause
"Missing IP-range and protocol validation in remote file fetching allows SSRF to private/reserved network addresses."
Attack vector
An authenticated administrator can trigger SSRF by navigating to "Media Upload/Import" (`/e107_admin/image.php?mode=main&action=import`) and specifying a local environment URL in the "Image/File URL:" field [ref_id=2]. The server then fetches the supplied URL without validating whether the target IP is private or reserved, enabling port scanning of internal services (e.g., `http://localhost:80/test.png` vs. `http://localhost:1234/test.png`) and potential access to sensitive information on the local network [ref_id=2]. The attack requires only administrator-level authentication and no special network position.
Affected code
The vulnerability resides in `e107_handlers/file_class.php` within the `getRemoteFile()` and `getRemoteContent()` methods. These methods previously passed admin-supplied URLs directly to cURL with no IP or protocol restrictions, allowing SSRF through the Media Manager's "From a remote location" import feature [ref_id=2].
What the fix does
Patch `5f98cc9f` [patch_id=2563973] adds `isUrlSafe()` to `e_file`, which rejects non-HTTP(S) schemes, resolves the host via DNS (A + AAAA records), and blocks any private, loopback, link-local, or reserved IP using PHP's `FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE`. It also pins `CURLOPT_PROTOCOLS` and `CURLOPT_REDIR_PROTOCOLS` to HTTP/HTTPS only, preventing protocol smuggling via redirects. Patch `40b2d111` [patch_id=2563972] adds `canonicalizeIp()` to normalize IPv4-mapped IPv6 addresses (e.g., `::ffff:127.0.0.1`) back to bare IPv4 before the range check, closing a filter-evasion vector where PHP's `filter_var` does not normalize the mapped form on its own [patch_id=2563972]. Sites that legitimately need intranet fetching can opt back in by defining `e_REMOTE_FILE_ALLOW_PRIVATE` to `true` in `e107_config.php` [patch_id=2563973].
Preconditions
- authAttacker must have administrator-level access to the e107 CMS
- configThe target e107 instance must be version 2.3.3 or earlier
- networkAttacker must be able to reach the admin panel over the network
- inputAttacker supplies a URL in the 'Image/File URL:' field of the Media Manager import form
Reproduction
Log in to the administrator screen and access "Media Upload/Import" (`/e107_admin/image.php?mode=main&action=import`). Specify a local environment URL in the "Image/File URL:" field — for example, `http://localhost:80/test.png` for an open port or `http://localhost:1234/test.png` for a closed port. Observe the response: an open port returns no error, while a closed port returns "Error: There was a problem grabbing the file" [ref_id=2].
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.