MDL-41819 try to work around max_input_vars restriction

This commit is contained in:
Petr Škoda
2013-12-02 08:30:47 +08:00
parent 828aff67d9
commit db217f006c
8 changed files with 199 additions and 71 deletions
+7 -28
View File
@@ -261,10 +261,11 @@ abstract class moodleform {
$submission = array();
if ($method == 'post') {
if (!empty($_POST)) {
$submission = $this->_get_post_params();
$submission = $_POST;
}
} else {
$submission = array_merge_recursive($_GET, $this->_get_post_params()); // Emulate handling of parameters in xxxx_param().
$submission = $_GET;
merge_query_params($submission, $_POST); // Emulate handling of parameters in xxxx_param().
}
// following trick is needed to enable proper sesskey checks when using GET forms
@@ -283,34 +284,12 @@ abstract class moodleform {
}
/**
* Internal method. Gets all POST variables, bypassing max_input_vars limit if needed.
*
* @return array All POST variables as an array, in the same format as $_POST.
* Internal method - should not be used anywhere.
* @deprecated since 2.6
* @return array $_POST.
*/
protected function _get_post_params() {
$enctype = $this->_form->getAttribute('enctype');
$max = (int)ini_get('max_input_vars');
if (empty($max) || count($_POST, COUNT_RECURSIVE) < $max || (!empty($enctype) && $enctype == 'multipart/form-data')) {
return $_POST;
}
// Large POST request with enctype supported by php://input.
// Parse php://input in chunks to bypass max_input_vars limit, which also applies to parse_str().
$allvalues = array();
$values = array();
$str = file_get_contents("php://input");
$delim = '&';
$fun = create_function('$p', 'return implode("'.$delim.'", $p);');
$chunks = array_map($fun, array_chunk(explode($delim, $str), $max));
foreach ($chunks as $chunk) {
parse_str($chunk, $values);
$allvalues = array_merge_recursive($allvalues, $values);
}
return $allvalues;
return $_POST;
}
/**
+4
View File
@@ -712,6 +712,10 @@ if (!empty($CFG->profilingenabled)) {
}
}
// Hack to get around max_input_vars restrictions,
// we need to do this after session init to have some basic DDoS protection.
workaround_max_input_vars();
// Process theme change in the URL.
if (!empty($CFG->allowthemechangeonurl) and !empty($_GET['theme'])) {
// we have to use _GET directly because we do not want this to interfere with _POST
+102
View File
@@ -944,6 +944,108 @@ function setup_get_remote_url() {
return $rurl;
}
/**
* Try to work around the 'max_input_vars' restriction if necessary.
*/
function workaround_max_input_vars() {
// Make sure this gets executed only once from lib/setup.php!
static $executed = false;
if ($executed) {
debugging('workaround_max_input_vars() must be called only once!');
return;
}
$executed = true;
if (!isset($_SERVER["CONTENT_TYPE"]) or strpos($_SERVER["CONTENT_TYPE"], 'multipart/form-data') !== false) {
// Not a post or 'multipart/form-data' which is not compatible with "php://input" reading.
return;
}
if (!isloggedin() or isguestuser()) {
// Only real users post huge forms.
return;
}
$max = (int)ini_get('max_input_vars');
if ($max <= 0) {
// Most probably PHP < 5.3.9 that does not implement this limit.
return;
}
if ($max >= 200000) {
// This value should be ok for all our forms, by setting it in php.ini
// admins may prevent any unexpected regressions caused by this hack.
// Note there is no need to worry about DDoS caused by making this limit very high
// because there are very many easier ways to DDoS any Moodle server.
return;
}
if (count($_POST, COUNT_RECURSIVE) < $max) {
return;
}
// Large POST request with enctype supported by php://input.
// Parse php://input in chunks to bypass max_input_vars limit, which also applies to parse_str().
$str = file_get_contents("php://input");
if ($str === false or $str === '') {
// Some weird error.
return;
}
$delim = '&';
$fun = create_function('$p', 'return implode("'.$delim.'", $p);');
$chunks = array_map($fun, array_chunk(explode($delim, $str), $max));
foreach ($chunks as $chunk) {
$values = array();
parse_str($chunk, $values);
if (ini_get_bool('magic_quotes_gpc')) {
// Use the same logic as lib/setup.php to work around deprecated magic quotes.
$values = array_map('stripslashes_deep', $values);
}
merge_query_params($_POST, $values);
merge_query_params($_REQUEST, $values);
}
}
/**
* Merge parsed POST chunks.
*
* NOTE: this is not perfect, but it should work in most cases hopefully.
*
* @param array $target
* @param array $values
*/
function merge_query_params(array &$target, array $values) {
if (isset($values[0]) and isset($target[0])) {
// This looks like a split [] array, lets verify the keys are continuous starting with 0.
$keys1 = array_keys($values);
$keys2 = array_keys($target);
if ($keys1 === array_keys($keys1) and $keys2 === array_keys($keys2)) {
foreach ($values as $v) {
$target[] = $v;
}
return;
}
}
foreach ($values as $k => $v) {
if (!isset($target[$k])) {
$target[$k] = $v;
continue;
}
if (is_array($target[$k]) and is_array($v)) {
merge_query_params($target[$k], $v);
continue;
}
// We should not get here unless there are duplicates in params.
$target[$k] = $v;
}
}
/**
* Initializes our performance info early.
*
+78
View File
@@ -142,4 +142,82 @@ class core_setuplib_testcase extends basic_testcase {
$this->assertContains($expected, $exceptioninfo->message, 'Exception message does not contain system paths');
$this->assertContains($expected, $exceptioninfo->debuginfo, 'Exception debug info does not contain system paths');
}
public function test_merge_query_params() {
$original = array(
'id' => '1',
'course' => '2',
'action' => 'delete',
'grade' => array(
0 => 'a',
1 => 'b',
2 => 'c',
),
'items' => array(
'a' => 'aa',
'b' => 'bb',
),
'mix' => array(
0 => '2',
),
'numerical' => array(
'2' => array('a' => 'b'),
'1' => '2',
),
);
$chunk = array(
'numerical' => array(
'0' => 'z',
'2' => array('d' => 'e'),
),
'action' => 'create',
'next' => '2',
'grade' => array(
0 => 'e',
1 => 'f',
2 => 'g',
),
'mix' => 'mix',
);
$expected = array(
'id' => '1',
'course' => '2',
'action' => 'create',
'grade' => array(
0 => 'a',
1 => 'b',
2 => 'c',
3 => 'e',
4 => 'f',
5 => 'g',
),
'items' => array(
'a' => 'aa',
'b' => 'bb',
),
'mix' => 'mix',
'numerical' => array(
'2' => array('a' => 'b', 'd' => 'e'),
'1' => '2',
'0' => 'z',
),
'next' => '2',
);
$array = $original;
merge_query_params($array, $chunk);
$this->assertSame($expected, $array);
$this->assertNotSame($original, $array);
$query = "id=1&course=2&action=create&grade%5B%5D=a&grade%5B%5D=b&grade%5B%5D=c&grade%5B%5D=e&grade%5B%5D=f&grade%5B%5D=g&items%5Ba%5D=aa&items%5Bb%5D=bb&mix=mix&numerical%5B2%5D%5Ba%5D=b&numerical%5B2%5D%5Bd%5D=e&numerical%5B1%5D=2&numerical%5B0%5D=z&next=2";
$decoded = array();
parse_str($query, $decoded);
$this->assertSame($expected, $decoded);
// Prove that we cannot use array_merge_recursive() instead.
$this->assertNotSame($expected, array_merge_recursive($original, $chunk));
}
}