@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow Phabricator to run with "enable_post_data_reading" disabled

Summary:
Ref T13008. Depends on D18701. The overall goal here is to make turning `enable_post_data_reading` off not break things, so we can run rate limiting checks before we read file uploads.

The biggest blocker for this is that turning it off stops `$_FILES` from coming into existence.

This //appears// to mostly work. Specifically:

- Skip the `max_post_size` check when POST is off, since it's meaningless.
- Don't read or scrub $_POST at startup when POST is off.
- When we rebuild REQUEST and POST before processing requests, do multipart parsing if we need to and rebuild FILES.
- Skip the `is_uploaded_file()` check if we built FILES ourselves.

This probably breaks a couple of small things, like maybe `__profile__` and other DarkConsole triggers over POST, and probably some other weird stuff. The parsers may also need more work than they've received so far.

I also need to verify that this actually works (i.e., lets us run code without reading the request body) but I'll include that in the change where I update the actual rate limiting.

Test Plan:
- Disabled `enable_post_data_reading`.
- Uploaded a file with a vanilla upload form (project profile image).
- Uploaded a file with drag and drop.
- Used DarkConsole.
- Submitted comments.
- Created a task.
- Browsed around.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13008

Differential Revision: https://secure.phabricator.com/D18702

+101 -24
+65 -12
src/aphront/configuration/AphrontDefaultApplicationConfiguration.php
··· 15 15 $parser = new PhutilQueryStringParser(); 16 16 $data = array(); 17 17 18 - // If the request has "multipart/form-data" content, we can't use 19 - // PhutilQueryStringParser to parse it, and the raw data supposedly is not 20 - // available anyway (according to the PHP documentation, "php://input" is 21 - // not available for "multipart/form-data" requests). However, it is 22 - // available at least some of the time (see T3673), so double check that 23 - // we aren't trying to parse data we won't be able to parse correctly by 24 - // examining the Content-Type header. 25 - $content_type = idx($_SERVER, 'CONTENT_TYPE'); 26 - $is_form_data = preg_match('@^multipart/form-data@i', $content_type); 27 - 28 18 $request_method = idx($_SERVER, 'REQUEST_METHOD'); 29 19 if ($request_method === 'PUT') { 30 20 // For PUT requests, do nothing: in particular, do NOT read input. This 31 21 // allows us to stream input later and process very large PUT requests, 32 22 // like those coming from Git LFS. 33 23 } else { 24 + // For POST requests, we're going to read the raw input ourselves here 25 + // if we can. Among other things, this corrects variable names with 26 + // the "." character in them, which PHP normally converts into "_". 27 + 28 + // There are two major considerations here: whether the 29 + // `enable_post_data_reading` option is set, and whether the content 30 + // type is "multipart/form-data" or not. 31 + 32 + // If `enable_post_data_reading` is off, we're free to read the entire 33 + // raw request body and parse it -- and we must, because $_POST and 34 + // $_FILES are not built for us. If `enable_post_data_reading` is on, 35 + // which is the default, we may not be able to read the body (the 36 + // documentation says we can't, but empirically we can at least some 37 + // of the time). 38 + 39 + // If the content type is "multipart/form-data", we need to build both 40 + // $_POST and $_FILES, which is involved. The body itself is also more 41 + // difficult to parse than other requests. 34 42 $raw_input = PhabricatorStartup::getRawInput(); 35 - if (strlen($raw_input) && !$is_form_data) { 36 - $data += $parser->parseQueryString($raw_input); 43 + if (strlen($raw_input)) { 44 + $content_type = idx($_SERVER, 'CONTENT_TYPE'); 45 + $is_multipart = preg_match('@^multipart/form-data@i', $content_type); 46 + if ($is_multipart && !ini_get('enable_post_data_reading')) { 47 + $multipart_parser = id(new AphrontMultipartParser()) 48 + ->setContentType($content_type); 49 + 50 + $multipart_parser->beginParse(); 51 + $multipart_parser->continueParse($raw_input); 52 + $parts = $multipart_parser->endParse(); 53 + 54 + $query_string = array(); 55 + foreach ($parts as $part) { 56 + if (!$part->isVariable()) { 57 + continue; 58 + } 59 + 60 + $name = $part->getName(); 61 + $value = $part->getVariableValue(); 62 + 63 + $query_string[] = urlencode($name).'='.urlencode($value); 64 + } 65 + $query_string = implode('&', $query_string); 66 + $post = $parser->parseQueryString($query_string); 67 + 68 + $files = array(); 69 + foreach ($parts as $part) { 70 + if ($part->isVariable()) { 71 + continue; 72 + } 73 + 74 + $files[$part->getName()] = $part->getPHPFileDictionary(); 75 + } 76 + $_FILES = $files; 77 + } else { 78 + $post = $parser->parseQueryString($raw_input); 79 + } 80 + 81 + $_POST = $post; 82 + PhabricatorStartup::rebuildRequest(); 83 + 84 + $data += $post; 37 85 } else if ($_POST) { 86 + $post = filter_input_array(INPUT_POST, FILTER_UNSAFE_RAW); 87 + if (is_array($post)) { 88 + $_POST = $post; 89 + PhabricatorStartup::rebuildRequest(); 90 + } 38 91 $data += $_POST; 39 92 } 40 93 }
+11 -3
src/applications/files/storage/PhabricatorFile.php
··· 178 178 } 179 179 180 180 $tmp_name = idx($spec, 'tmp_name'); 181 - $is_valid = @is_uploaded_file($tmp_name); 182 - if (!$is_valid) { 183 - throw new Exception(pht('File is not an uploaded file.')); 181 + 182 + // NOTE: If we parsed the request body ourselves, the files we wrote will 183 + // not be registered in the `is_uploaded_file()` list. It's fine to skip 184 + // this check: it just protects against sloppy code from the long ago era 185 + // of "register_globals". 186 + 187 + if (ini_get('enable_post_data_reading')) { 188 + $is_valid = @is_uploaded_file($tmp_name); 189 + if (!$is_valid) { 190 + throw new Exception(pht('File is not an uploaded file.')); 191 + } 184 192 } 185 193 186 194 $file_data = Filesystem::readFile($tmp_name);
+25 -9
support/PhabricatorStartup.php
··· 416 416 417 417 // NOTE: We don't filter INPUT_SERVER because we don't want to overwrite 418 418 // changes made in "preamble.php". 419 + 420 + // NOTE: WE don't filter INPUT_POST because we may be constructing it 421 + // lazily if "enable_post_data_reading" is disabled. 422 + 419 423 $filter = array( 420 424 INPUT_GET, 421 - INPUT_POST, 422 425 INPUT_ENV, 423 426 INPUT_COOKIE, 424 427 ); ··· 434 437 case INPUT_COOKIE: 435 438 $_COOKIE = array_merge($_COOKIE, $filtered); 436 439 break; 437 - case INPUT_POST: 438 - $_POST = array_merge($_POST, $filtered); 439 - break; 440 440 case INPUT_ENV; 441 441 $env = array_merge($_ENV, $filtered); 442 442 $_ENV = self::filterEnvSuperglobal($env); ··· 444 444 } 445 445 } 446 446 447 - // rebuild $_REQUEST, respecting order declared in ini files 447 + self::rebuildRequest(); 448 + } 449 + 450 + /** 451 + * @task validation 452 + */ 453 + public static function rebuildRequest() { 454 + // Rebuild $_REQUEST, respecting order declared in ".ini" files. 448 455 $order = ini_get('request_order'); 456 + 449 457 if (!$order) { 450 458 $order = ini_get('variables_order'); 451 459 } 460 + 452 461 if (!$order) { 453 - // $_REQUEST will be empty, leave it alone 462 + // $_REQUEST will be empty, so leave it alone. 454 463 return; 455 464 } 465 + 456 466 $_REQUEST = array(); 457 - for ($i = 0; $i < strlen($order); $i++) { 458 - switch ($order[$i]) { 467 + for ($ii = 0; $ii < strlen($order); $ii++) { 468 + switch ($order[$ii]) { 459 469 case 'G': 460 470 $_REQUEST = array_merge($_REQUEST, $_GET); 461 471 break; ··· 466 476 $_REQUEST = array_merge($_REQUEST, $_COOKIE); 467 477 break; 468 478 default: 469 - // $_ENV and $_SERVER never go into $_REQUEST 479 + // $_ENV and $_SERVER never go into $_REQUEST. 470 480 break; 471 481 } 472 482 } ··· 590 600 private static function detectPostMaxSizeTriggered() { 591 601 // If this wasn't a POST, we're fine. 592 602 if ($_SERVER['REQUEST_METHOD'] != 'POST') { 603 + return; 604 + } 605 + 606 + // If "enable_post_data_reading" is off, we won't have $_POST and this 607 + // condition is effectively impossible. 608 + if (!ini_get('enable_post_data_reading')) { 593 609 return; 594 610 } 595 611