1
0
Fork 0
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:
epriestley 2020-04-14 14:23:32 -07:00
parent 99cbc20778
commit 6b05d2be28
3 changed files with 149 additions and 1 deletions

View file

@ -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),

View file

@ -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;
}
}

View file

@ -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);
}
}