Repository navigation
Possible SSRF Exploitation via Scheme Validation Bypass #573
Description
Activity
I can confirm that this is technically an SSRF vulnerability. I have provided a resolution in #578 for review. Thanks!
Reacted by Vítor de OliveiraThanks for working on this! I tested the branch locally against master (c45a900) and found a few issues:
- The main PoC from Possible SSRF Exploitation via Scheme Validation Bypass #573 is still exploitable.
isValidUrl()is only called fromExtractor::resolveUri(), butEmbed::get()/getMulti()send the request directly via the Crawler.$embed->get('http://127.0.0.1:PORT/secret')(and 169.254.169.254, 10.x, localhost) still fetch and return the internal page. HTTP redirects (curlFOLLOWLOCATION) to internal hosts are also not checked. - Regression with relative URLs. Making
isHttp()return false for scheme-less strings meansresolveUri('/img.png'),'../x','//cdn.example.com/x'now throw. Relativeog:image, icons, feeds and oEmbed links are silently dropped, and$info->languagesthrows an uncaughtInvalidArgumentExceptionon pages with relativehreflanglinks. The suite goes from 1 failure on master to 18 (17 new failures inPagesTest). - Range gaps:
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGEstill allows100.100.100.200(Alibaba metadata),100.64.0.0/10,198.18.0.0/15and64:ff9b::/96. ConsiderFILTER_FLAG_GLOBAL_RANGE(PHP >= 8.2) or an explicit blocklist.FILTER_VALIDATE_URLalso rejects valid hosts with underscores and non-punycode IDNs. - The new tests depend on live DNS (
example.com,foo.com), which makes them flaky offline/in CI.
Suggestion: keep
isHttp()permissive for relative refs and validate the resolved absolute URI afterresolveUri(). Add the same check inEmbed::get()/getMulti()and on every redirect hop (or disableFOLLOWLOCATIONand follow redirects manually). Ideally pin the validated IP withCURLOPT_RESOLVEto avoid DNS rebinding, and restrictCURLOPT_PROTOCOLS/CURLOPT_REDIR_PROTOCOLSto HTTP(S).- The main PoC from Possible SSRF Exploitation via Scheme Validation Bypass #573 is still exploitable.
Thanks for the feedback, @Vitorinox . I've added a series of commits that address the remaining issues you called out. Full disclosure: some of the reworking of cURL behavior was beyond my area of expertise, so I used an LLM to assist with staging that code. I then performed a code review to confirm that it looked technically accurate and can confirm that automated tests are continuing to behave the same way as they were before. I also added new test coverage to demonstrate that direct calls to
Embed::get()andEmbed::getMulti()have URL validation. Summary of changes:Regression with relative URLs. Making isHttp() return false for scheme-less strings means resolveUri('/img.png'), '../x', '//cdn.example.com/x' now throw ... Suggestion: keep isHttp() permissive for relative refs
This is done in 31aa303
Range gaps: FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE still allows 100.100.100.200 (Alibaba metadata), 100.64.0.0/10, 198.18.0.0/15 and 64:ff9b::/96. Consider FILTER_FLAG_GLOBAL_RANGE (PHP >= 8.2) or an explicit blocklist.
These range gaps are covered using FILTER_FLAG_GLOBAL_RANGE, with test coverage added, in 3faf69e . I chose to leave this fix for PHP >= 8.2 rather than creating an explicit blocklist; if you think it's important to provide coverage for PHP < 8.2, let me know.
The main PoC from #573 is still exploitable. isValidUrl() is only called from Extractor::resolveUri(), but Embed::get()/getMulti() send the request directly via the Crawler.
Done in 1bbcb91 , with test coverage demonstrating the fix.
I also added checking after the RedirectUri is obtained in 24661be
Ideally pin the validated IP with CURLOPT_RESOLVE to avoid DNS rebinding
This is the most complicated code change, replacing default cURL parameters with custom ones to avoid SSRF rebinding in scenarios where a DNS has an extremely short or nonexistent TTL: 35c8560 . This also adds a
getValidUrlIps()wrapper to return an array of valid IPs, relevant for URLs with redirects.restrict CURLOPT_PROTOCOLS/CURLOPT_REDIR_PROTOCOLS to HTTP(S).
This and other hardening is performed in
d6f6915
3550e83
0810befThanks for addressing the SSRF issue and the additional bypasses.
Since the vulnerability has now been patched and regression tests have been added, I'd like to request that this be tracked as a GitHub Security Advisory and assigned a CVE.
Could you please create a draft security advisory for the issue and request a CVE ID through GitHub?
I'm happy to provide any additional information needed for the advisory, including affected versions, the original PoC, impact, and the timeline.
Please also credit
karthi-the-hackeras the security researcher in the advisory.- added a commit that references this issue
on Oct 9, 2026
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: