public function testQueryStringListParsing() { $map = array('' => array(), '&' => array(), '=' => array(array('', '')), '=&' => array(array('', '')), 'a=b' => array(array('a', 'b')), 'a[]=b' => array(array('a[]', 'b')), 'a=' => array(array('a', '')), '. [=1' => array(array('. [', '1')), 'a=b&c=d' => array(array('a', 'b'), array('c', 'd')), 'a=b&a=c' => array(array('a', 'b'), array('a', 'c')), '&a=b&' => array(array('a', 'b')), '=a' => array(array('', 'a')), '&&&' => array(), 'a%20b=c%20d' => array(array('a b', 'c d'))); $parser = new PhutilQueryStringParser(); foreach ($map as $query_string => $expected) { $this->assertEqual($expected, $parser->parseQueryStringToPairList($query_string)); } }
/** * Produces a value safe to pass to `CURLOPT_POSTFIELDS`. * * @return wild Some value, suitable for use in `CURLOPT_POSTFIELDS`. */ private function formatRequestDataForCURL() { // We're generating a value to hand to cURL as CURLOPT_POSTFIELDS. The way // cURL handles this value has some tricky caveats. // First, we can return either an array or a query string. If we return // an array, we get a "multipart/form-data" request. If we return a // query string, we get an "application/x-www-form-urlencoded" request. // Second, if we return an array we can't duplicate keys. The user might // want to send the same parameter multiple times. // Third, if we return an array and any of the values start with "@", // cURL includes arbitrary files off disk and sends them to an untrusted // remote server. For example, an array like: // // array('name' => '@/usr/local/secret') // // ...will attempt to read that file off disk and transmit its contents with // the request. This behavior is pretty surprising, and it can easily // become a relatively severe security vulnerability which allows an // attacker to read any file the HTTP process has access to. Since this // feature is very dangerous and not particularly useful, we prevent its // use. Broadly, this means we must reject some requests because they // contain an "@" in an inconvenient place. // Generally, to avoid the "@" case and because most servers usually // expect "application/x-www-form-urlencoded" data, we try to return a // string unless there are files attached to this request. $data = $this->getData(); $files = $this->files; $any_data = $data || is_string($data) && strlen($data); $any_files = (bool) $this->files; if (!$any_data && !$any_files) { // No files or data, so just bail. return null; } if (!$any_files) { // If we don't have any files, just encode the data as a query string, // make sure it's not including any files, and we're good to go. if (is_array($data)) { $data = http_build_query($data, '', '&'); } $this->checkForDangerousCURLMagic($data, $is_query_string = true); return $data; } // If we've made it this far, we have some files, so we need to return // an array. First, convert the other data into an array if it isn't one // already. if (is_string($data)) { // NOTE: We explicitly don't want fancy array parsing here, so just // do a basic parse and then convert it into a dictionary ourselves. $parser = new PhutilQueryStringParser(); $pairs = $parser->parseQueryStringToPairList($data); $map = array(); foreach ($pairs as $pair) { list($key, $value) = $pair; if (array_key_exists($key, $map)) { throw new Exception(pht('Request specifies two values for key "%s", but parameter ' . 'names must be unique if you are posting file data due to ' . 'limitations with cURL.', $key)); } $map[$key] = $value; } $data = $map; } foreach ($data as $key => $value) { $this->checkForDangerousCURLMagic($value, $is_query_string = false); } foreach ($this->files as $name => $info) { if (array_key_exists($name, $data)) { throw new Exception(pht('Request specifies a file with key "%s", but that key is also ' . 'defined by normal request data. Due to limitations with cURL, ' . 'requests that post file data must use unique keys.', $name)); } $tmp = new TempFile($info['name']); Filesystem::writeFile($tmp, $info['data']); $this->temporaryFiles[] = $tmp; // In 5.5.0 and later, we can use CURLFile. Prior to that, we have to // use this "@" stuff. if (class_exists('CURLFile', false)) { $file_value = new CURLFile((string) $tmp, $info['mime'], $info['name']); } else { $file_value = '@' . (string) $tmp; } $data[$name] = $file_value; } return $data; }