mirror of
https://we.phorge.it/source/phorge.git
synced 2025-04-10 11:28:35 +02: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(
|
$result = array(
|
||||||
'path' => $path,
|
'path' => $path,
|
||||||
'params' => $params,
|
'params' => $params,
|
||||||
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
|
'user' => idx($_SERVER, 'PHP_AUTH_USER'),
|
||||||
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
|
'pass' => idx($_SERVER, 'PHP_AUTH_PW'),
|
||||||
|
|
||||||
|
'raw.base64' => $base64_input,
|
||||||
|
|
||||||
// This just makes sure that the response compresses well, so reasonable
|
// This just makes sure that the response compresses well, so reasonable
|
||||||
// algorithms should want to gzip or deflate it.
|
// algorithms should want to gzip or deflate it.
|
||||||
'filler' => str_repeat('Q', 1024 * 16),
|
'filler' => str_repeat('Q', 1024 * 16),
|
||||||
|
|
|
@ -89,4 +89,24 @@ final class AphrontRequestStream extends Phobject {
|
||||||
return $stream;
|
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))
|
new PhutilOpaqueEnvelope($expect_pass))
|
||||||
->setTimeout(5);
|
->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,
|
// 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
|
// 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.
|
// this is a bad idea. Outside of AWS, this request will just fail.
|
||||||
|
@ -61,12 +75,16 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
|
||||||
$self_future,
|
$self_future,
|
||||||
$ec2_future,
|
$ec2_future,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($gzip_future) {
|
||||||
|
$futures[] = $gzip_future;
|
||||||
|
}
|
||||||
|
|
||||||
$futures = new FutureIterator($futures);
|
$futures = new FutureIterator($futures);
|
||||||
foreach ($futures as $future) {
|
foreach ($futures as $future) {
|
||||||
// Just resolve the futures here.
|
// Just resolve the futures here.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
list($body) = $ec2_future->resolvex();
|
list($body) = $ec2_future->resolvex();
|
||||||
$body = trim($body);
|
$body = trim($body);
|
||||||
|
@ -259,6 +277,107 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck {
|
||||||
->setMessage($message);
|
->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