|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2021-06-10 02:37 UTC] vi at hackberry dot xyz
Description: ------------ In reports https://bugs.php.net/bug.php?id=77423 and https://bugs.php.net/bug.php?id=81116, it is suggested to use FILTER_VALIDATE_URL but I have found a bypass that allows bypassing FILTER_VALIDATE_URL check. Test script: --------------- echo filter_var("https://example.com:\@test.com/", FILTER_VALIDATE_URL) Expected result: ---------------- Should not validate as a valid URL given the URL. Actual result: -------------- Validates URL as valid. This payload in file_get_contents and parse_url would treat test.com as host. PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 11:00:01 2025 UTC |
Consider the following code: <?php if(filter_var($_GET['url'], FILTER_VALIDATE_URL)) { header("location: ".$_GET['url']); } Now if you visit https://localhost/?url=https://example.com:\@test.com The browser will redirect to https://example.com/@test.com. This can be used to bypass any open redirect mitigations as well as introduce a discripency since from PHP's perspective, the host is test.com here but for the browser, host is example.com Now consider an SSRF protection which uses parse_url to check for test.com as host: <?php if(filter_var($_GET['url'], FILTER_VALIDATE_URL)) { if("test.com" === parse_url($_GET['url'])['host']) { header("location: ".$_GET['url']); } } The above will pass for the payload https://example.com:\@test.com but browser will redirect to https://example.com/@test.com Now another case where file_get_contents is used over username and password provided by a user: <?php $username = $_GET['user']; $password = $_GET['pass']; $loginurl = 'https://' . $username . ':' . $password . '@test.com/'; if(filter_var($loginurl, FILTER_VALIDATE_URL)) { echo file_get_contents($loginurl); } For the username as 'example.com' and password as '/', the request will be sent to https://example.com/@test.com since ':' will indicate start of port. But port will end with '/' that indicates the start of path. Another case where we allow `\` in the password: parse_url("https://user:\epass@test.com") would return [ "scheme" => "https", "host" => "test.com", "user" => "user", "pass" => "_pass", ] and the following: parse_url("https://user:\\@test.com") would return [ "scheme" => "https", "host" => "test.com", "user" => "user", "pass" => "\", ] Another case I noticed, parse_url("https://example.com:\/@test.com") would return false which is completely unexpected as '\' should have escaped '/' resulting in: [ "scheme" => "https", "host" => "example.com", "path" => "/@test.com", ]> This seems to be a valid URL with username "example.com" and > password "\". According to RFC 3986[1], it is not. userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" The backslash would have to be percent encoded. Since it is not, apparently browsers interpret :\ as slash, here. [1] <https://datatracker.ietf.org/doc/html/rfc3986>While fixing this, also take the following case in consideration: --- parse_url("https://example.com:80\/@asdf.com"); => [ "scheme" => "https", "host" => "example.com", "port" => 80, "path" => "/@asdf.com", ] --- Here \/ becomes a path separator making 80 a port number which I think is due to / getting escaped by backslash. Tested on PHP 8.0.7Exclamation mark is fine, but caret is not: Other characters are excluded because gateways and other transport agents are known to sometimes modify such characters, or they are used as delimiters. unwise = "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"> The password contains a caret character and an exclamation mark, > everything else is alphanumberic. According to RFC 3986[1], a caret is not valid in the password which is a part of the userinfo: userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" The caret would need to be percent-encoded as %5E. [1] <https://datatracker.ietf.org/doc/html/rfc3986>