mirror of
https://we.phorge.it/source/phorge.git
synced 2025-03-28 12:08:14 +01:00
Add a setup warning to detect "SetInputFilter DEFLATE" and other "Content-Encoding" request mangling
Summary: Ref T13507. See that task for discussion. Test Plan: Faked different response behaviors and hit both variations of this error. Maniphest Tasks: T13507 Differential Revision: https://secure.phabricator.com/D21116
This commit is contained in:
parent
99cbc20778
commit
6b05d2be28
3 changed files with 149 additions and 1 deletions
|
@ -771,12 +771,21 @@ final class AphrontApplicationConfiguration
|
|||
);
|
||||
}
|
||||
|
||||
$raw_input = @file_get_contents('php://input');
|
||||
if ($raw_input !== false) {
|
||||
$base64_input = base64_encode($raw_input);
|
||||
} else {
|
||||
$base64_input = null;
|
||||
}
|
||||
|
||||
$result = array(
|
||||
'path' => $path,
|
||||
'params' => $params,
|
||||
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
|
||||
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
|
||||
|
||||
'raw.base64' => $base64_input,
|
||||
|
||||
// This just makes sure that the response compresses well, so reasonable
|
||||
// algorithms should want to gzip or deflate it.
|
||||
'filler' => str_repeat('Q', 1024 * 16),
|
||||
|
|
|
@ -89,4 +89,24 @@ final class AphrontRequestStream extends Phobject {
|
|||
return $stream;
|
||||
}
|
||||
|
||||
public static function supportsGzip() {
|
||||
if (!function_exists('gzencode') || !function_exists('gzdecode')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$has_zlib = false;
|
||||
|
||||
// NOTE: At least locally, this returns "zlib.*", which is not terribly
|
||||
// reassuring. We care about "zlib.inflate".
|
||||
|
||||
$filters = stream_get_filters();
|
||||
foreach ($filters as $filter) {
|
||||
if (preg_match('/^zlib\\./', $filter)) {
|
||||
$has_zlib = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $has_zlib;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -50,6 +50,20 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
|
|||
new PhutilOpaqueEnvelope($expect_pass))
|
||||
->setTimeout(5);
|
||||
|
||||
if (AphrontRequestStream::supportsGzip()) {
|
||||
$gzip_uncompressed = str_repeat('Quack! ', 128);
|
||||
$gzip_compressed = gzencode($gzip_uncompressed);
|
||||
|
||||
$gzip_future = id(new HTTPSFuture($base_uri))
|
||||
->addHeader('X-Phabricator-SelfCheck', 1)
|
||||
->addHeader('Content-Encoding', 'gzip')
|
||||
->setTimeout(5)
|
||||
->setData($gzip_compressed);
|
||||
|
||||
} else {
|
||||
$gzip_future = null;
|
||||
}
|
||||
|
||||
// Make a request to the metadata service available on EC2 instances,
|
||||
// to test if we're running on a T2 instance in AWS so we can warn that
|
||||
// this is a bad idea. Outside of AWS, this request will just fail.
|
||||
|
@ -61,12 +75,16 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
|
|||
$self_future,
|
||||
$ec2_future,
|
||||
);
|
||||
|
||||
if ($gzip_future) {
|
||||
$futures[] = $gzip_future;
|
||||
}
|
||||
|
||||
$futures = new FutureIterator($futures);
|
||||
foreach ($futures as $future) {
|
||||
// Just resolve the futures here.
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
list($body) = $ec2_future->resolvex();
|
||||
$body = trim($body);
|
||||
|
@ -259,6 +277,107 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
|
|||
->setMessage($message);
|
||||
}
|
||||
|
||||
if ($gzip_future) {
|
||||
$this->checkGzipResponse(
|
||||
$gzip_future,
|
||||
$gzip_uncompressed,
|
||||
$gzip_compressed);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkGzipResponse(
|
||||
Future $future,
|
||||
$uncompressed,
|
||||
$compressed) {
|
||||
|
||||
try {
|
||||
list($body, $headers) = $future->resolvex();
|
||||
} catch (Exception $ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$structure = phutil_json_decode(trim($body));
|
||||
} catch (Exception $ex) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw_body = idx($structure, 'raw.base64');
|
||||
$raw_body = base64_decode($raw_body);
|
||||
|
||||
// The server received the exact compressed bytes we expected it to, so
|
||||
// everything is working great.
|
||||
if ($raw_body === $compressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the server received a prefix of the raw uncompressed string, it
|
||||
// is almost certainly configured to decompress responses inline. Guide
|
||||
// users to this problem narrowly.
|
||||
|
||||
// Otherwise, something is wrong but we don't have much of a clue what.
|
||||
|
||||
$message = array();
|
||||
$message[] = pht(
|
||||
'Phabricator sent itself a test request that was compressed with '.
|
||||
'"Content-Encoding: gzip", but received different bytes than it '.
|
||||
'sent.');
|
||||
|
||||
$prefix_len = min(strlen($raw_body), strlen($uncompressed));
|
||||
if ($prefix_len > 16 && !strncmp($raw_body, $uncompressed, $prefix_len)) {
|
||||
$message[] = pht(
|
||||
'The request body that the server received had already been '.
|
||||
'decompressed. This strongly suggests your webserver is configured '.
|
||||
'to decompress requests inline, before they reach PHP.');
|
||||
$message[] = pht(
|
||||
'If you are using Apache, your server may be configured with '.
|
||||
'"SetInputFilter DEFLATE". This directive destructively mangles '.
|
||||
'requests and emits them with "Content-Length" and '.
|
||||
'"Content-Encoding" headers that no longer match the data in the '.
|
||||
'request body.');
|
||||
} else {
|
||||
$message[] = pht(
|
||||
'This suggests your webserver is configured to decompress or mangle '.
|
||||
'compressed requests.');
|
||||
|
||||
$message[] = pht(
|
||||
'The request body Phabricator sent began:');
|
||||
$message[] = $this->snipBytes($compressed);
|
||||
|
||||
$message[] = pht(
|
||||
'The request body Phabricator received began:');
|
||||
$message[] = $this->snipBytes($raw_body);
|
||||
}
|
||||
|
||||
$message[] = pht(
|
||||
'Identify the component in your webserver configuration which is '.
|
||||
'decompressing or mangling requests and disable it. Phabricator '.
|
||||
'will not work properly until you do.');
|
||||
|
||||
$message = phutil_implode_html("\n\n", $message);
|
||||
|
||||
$this->newIssue('webserver.accept-gzip')
|
||||
->setName(pht('Compressed Requests Not Received Properly'))
|
||||
->setSummary(
|
||||
pht(
|
||||
'Your webserver is not handling compressed request bodies '.
|
||||
'properly.'))
|
||||
->setMessage($message);
|
||||
}
|
||||
|
||||
private function snipBytes($raw) {
|
||||
if (!strlen($raw)) {
|
||||
$display = pht('<empty>');
|
||||
} else {
|
||||
$snip = substr($raw, 0, 24);
|
||||
$display = phutil_loggable_string($snip);
|
||||
|
||||
if (strlen($snip) < strlen($raw)) {
|
||||
$display .= '...';
|
||||
}
|
||||
}
|
||||
|
||||
return phutil_tag('tt', array(), $display);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue