mirror of
https://we.phorge.it/source/phorge.git
synced 2025-02-19 18:28:39 +01:00
Provide software protections for HTTP response splitting
Summary: This addresses a few things: - Provide a software HTTP response spliting guard as an extra layer of security, see http://news.php.net/php.internals/57655 and who knows what HPHP/i does. - Cleans up webroot/index.php a little bit, I want to get that file under control eventually. - Eventually I want to collect bytes in/out metrics and this allows us to do that easily. - We may eventually want to write to a socket or do something else like that, ala Litespawn. Test Plan: - Ran unit tests. - Browsed around, checked headers and HTTP status codes. Reviewers: btrahan, vrana Reviewed By: btrahan CC: aran, epriestley Differential Revision: https://secure.phabricator.com/D1564
This commit is contained in:
parent
be424bf381
commit
e8a7d8a905
10 changed files with 348 additions and 11 deletions
|
@ -48,15 +48,19 @@ phutil_register_library_map(array(
|
|||
'AphrontFormToggleButtonsControl' => 'view/form/control/togglebuttons',
|
||||
'AphrontFormTokenizerControl' => 'view/form/control/tokenizer',
|
||||
'AphrontFormView' => 'view/form/base',
|
||||
'AphrontHTTPSink' => 'aphront/sink/base',
|
||||
'AphrontHTTPSinkTestCase' => 'aphront/sink/base/__tests__',
|
||||
'AphrontHeadsupActionListView' => 'view/layout/headsup/actionlist',
|
||||
'AphrontHeadsupActionView' => 'view/layout/headsup/action',
|
||||
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
|
||||
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
|
||||
'AphrontJavelinView' => 'view/javelin-view',
|
||||
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts',
|
||||
'AphrontListFilterView' => 'view/layout/listfilter',
|
||||
'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
|
||||
'AphrontNullView' => 'view/null',
|
||||
'AphrontPHPHTTPSink' => 'aphront/sink/php',
|
||||
'AphrontPageView' => 'view/page/base',
|
||||
'AphrontPagerView' => 'view/control/pager',
|
||||
'AphrontPanelView' => 'view/layout/panel',
|
||||
|
@ -868,15 +872,18 @@ phutil_register_library_map(array(
|
|||
'AphrontFormToggleButtonsControl' => 'AphrontFormControl',
|
||||
'AphrontFormTokenizerControl' => 'AphrontFormControl',
|
||||
'AphrontFormView' => 'AphrontView',
|
||||
'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontHeadsupActionListView' => 'AphrontView',
|
||||
'AphrontHeadsupActionView' => 'AphrontView',
|
||||
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
||||
'AphrontJavelinView' => 'AphrontView',
|
||||
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
||||
'AphrontListFilterView' => 'AphrontView',
|
||||
'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||
'AphrontNullView' => 'AphrontView',
|
||||
'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
|
||||
'AphrontPageView' => 'AphrontView',
|
||||
'AphrontPagerView' => 'AphrontView',
|
||||
'AphrontPanelView' => 'AphrontView',
|
||||
|
|
113
src/aphront/sink/base/AphrontHTTPSink.php
Normal file
113
src/aphront/sink/base/AphrontHTTPSink.php
Normal file
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Abstract class which wraps some sort of output mechanism for HTTP responses.
|
||||
* Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and
|
||||
* "header()" to emit responses.
|
||||
*
|
||||
* Mostly, this class allows us to do install security or metrics hooks in the
|
||||
* output pipeline.
|
||||
*
|
||||
* @task write Writing Response Components
|
||||
* @task emit Emitting the Response
|
||||
*
|
||||
* @group aphront
|
||||
*/
|
||||
abstract class AphrontHTTPSink {
|
||||
|
||||
|
||||
/* -( Writing Response Components )---------------------------------------- */
|
||||
|
||||
|
||||
/**
|
||||
* Write an HTTP status code to the output.
|
||||
*
|
||||
* @param int Numeric HTTP status code.
|
||||
* @return void
|
||||
*/
|
||||
final public function writeHTTPStatus($code) {
|
||||
if (!preg_match('/^\d{3}$/', $code)) {
|
||||
throw new Exception("Malformed HTTP status code '{$code}'!");
|
||||
}
|
||||
|
||||
$code = (int)$code;
|
||||
$this->emitHTTPStatus($code);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write HTTP headers to the output.
|
||||
*
|
||||
* @param list<pair> List of <name, value> pairs.
|
||||
* @return void
|
||||
*/
|
||||
final public function writeHeaders(array $headers) {
|
||||
foreach ($headers as $header) {
|
||||
if (!is_array($header) || count($header) !== 2) {
|
||||
throw new Exception('Malformed header.');
|
||||
}
|
||||
list($name, $value) = $header;
|
||||
|
||||
if (strpos($name, ':') !== false) {
|
||||
throw new Exception(
|
||||
"Declining to emit response with malformed HTTP header name: ".
|
||||
$name);
|
||||
}
|
||||
|
||||
// Attackers may perform an "HTTP response splitting" attack by making
|
||||
// the application emit certain types of headers containing newlines:
|
||||
//
|
||||
// http://en.wikipedia.org/wiki/HTTP_response_splitting
|
||||
//
|
||||
// PHP has built-in protections against HTTP response-splitting, but they
|
||||
// are of dubious trustworthiness:
|
||||
//
|
||||
// http://news.php.net/php.internals/57655
|
||||
|
||||
if (preg_match('/[\r\n\0]/', $name.$value)) {
|
||||
throw new Exception(
|
||||
"Declining to emit response with unsafe HTTP header: ".
|
||||
"<'".$name."', '".$value."'>.");
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($headers as $header) {
|
||||
$this->emitHeader($name, $value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write HTTP body data to the output.
|
||||
*
|
||||
* @param string Body data.
|
||||
* @return void
|
||||
*/
|
||||
final public function writeData($data) {
|
||||
$this->emitData($data);
|
||||
}
|
||||
|
||||
|
||||
/* -( Emitting the Response )---------------------------------------------- */
|
||||
|
||||
|
||||
abstract protected function emitHTTPStatus($code);
|
||||
abstract protected function emitHeader($name, $value);
|
||||
abstract protected function emitData($data);
|
||||
}
|
10
src/aphront/sink/base/__init__.php
Normal file
10
src/aphront/sink/base/__init__.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
||||
phutil_require_source('AphrontHTTPSink.php');
|
82
src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php
Normal file
82
src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
final class AphrontHTTPSinkTestCase extends PhabricatorTestCase {
|
||||
|
||||
public function testHTTPSinkBasics() {
|
||||
$sink = new AphrontIsolatedHTTPSink();
|
||||
$sink->writeHTTPStatus(200);
|
||||
$sink->writeHeaders(array(array('X-Test', 'test')));
|
||||
$sink->writeData('test');
|
||||
|
||||
$this->assertEqual(200, $sink->getEmittedHTTPStatus());
|
||||
$this->assertEqual(
|
||||
array(array('X-Test', 'test')),
|
||||
$sink->getEmittedHeaders());
|
||||
$this->assertEqual('test', $sink->getEmittedData());
|
||||
}
|
||||
|
||||
public function testHTTPSinkStatusCode() {
|
||||
$input = $this->tryTestCaseMap(
|
||||
array(
|
||||
200 => true,
|
||||
'201' => true,
|
||||
1 => false,
|
||||
1000 => false,
|
||||
'apple' => false,
|
||||
'' => false,
|
||||
),
|
||||
array($this, 'tryHTTPSinkStatusCode'));
|
||||
}
|
||||
|
||||
protected function tryHTTPSinkStatusCode($input) {
|
||||
$sink = new AphrontIsolatedHTTPSink();
|
||||
$sink->writeHTTPStatus($input);
|
||||
}
|
||||
|
||||
public function testHTTPSinkResponseSplitting() {
|
||||
$input = $this->tryTestCaseMap(
|
||||
array(
|
||||
"test" => true,
|
||||
"test\nx" => false,
|
||||
"test\rx" => false,
|
||||
"test\0x" => false,
|
||||
),
|
||||
array($this, 'tryHTTPSinkResponseSplitting'));
|
||||
}
|
||||
|
||||
protected function tryHTTPSinkResponseSplitting($input) {
|
||||
$sink = new AphrontIsolatedHTTPSink();
|
||||
$sink->writeHeaders(array(array('X-Test', $input)));
|
||||
}
|
||||
|
||||
public function testHTTPHeaderNames() {
|
||||
$input = $this->tryTestCaseMap(
|
||||
array(
|
||||
'test' => true,
|
||||
'test:' => false,
|
||||
),
|
||||
array($this, 'tryHTTPHeaderNames'));
|
||||
}
|
||||
|
||||
protected function tryHTTPHeaderNames($input) {
|
||||
$sink = new AphrontIsolatedHTTPSink();
|
||||
$sink->writeHeaders(array(array($input, 'value')));
|
||||
}
|
||||
|
||||
}
|
13
src/aphront/sink/base/__tests__/__init__.php
Normal file
13
src/aphront/sink/base/__tests__/__init__.php
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/sink/test');
|
||||
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
|
||||
|
||||
|
||||
phutil_require_source('AphrontHTTPSinkTestCase.php');
|
40
src/aphront/sink/php/AphrontPHPHTTPSink.php
Normal file
40
src/aphront/sink/php/AphrontPHPHTTPSink.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Concrete HTTP sink which uses "echo" and "header()" to emit data.
|
||||
*
|
||||
* @group aphront
|
||||
*/
|
||||
final class AphrontPHPHTTPSink extends AphrontHTTPSink {
|
||||
|
||||
protected function emitHTTPStatus($code) {
|
||||
if ($code != 200) {
|
||||
header("HTTP/1.0 {$code}");
|
||||
}
|
||||
}
|
||||
|
||||
protected function emitHeader($name, $value) {
|
||||
header("{$name}: {$value}", $replace = false);
|
||||
}
|
||||
|
||||
protected function emitData($data) {
|
||||
echo $data;
|
||||
}
|
||||
|
||||
}
|
12
src/aphront/sink/php/__init__.php
Normal file
12
src/aphront/sink/php/__init__.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/sink/base');
|
||||
|
||||
|
||||
phutil_require_source('AphrontPHPHTTPSink.php');
|
54
src/aphront/sink/test/AphrontIsolatedHTTPSink.php
Normal file
54
src/aphront/sink/test/AphrontIsolatedHTTPSink.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2012 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Isolated HTTP sink for testing.
|
||||
*
|
||||
* @group aphront
|
||||
*/
|
||||
final class AphrontIsolatedHTTPSink extends AphrontHTTPSink {
|
||||
|
||||
private $status;
|
||||
private $headers;
|
||||
private $data;
|
||||
|
||||
protected function emitHTTPStatus($code) {
|
||||
$this->status = $code;
|
||||
}
|
||||
|
||||
protected function emitHeader($name, $value) {
|
||||
$this->headers[] = array($name, $value);
|
||||
}
|
||||
|
||||
protected function emitData($data) {
|
||||
$this->data .= $data;
|
||||
}
|
||||
|
||||
public function getEmittedHTTPStatus() {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function getEmittedHeaders() {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getEmittedData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
}
|
12
src/aphront/sink/test/__init__.php
Normal file
12
src/aphront/sink/test/__init__.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is automatically generated. Lint this module to rebuild it.
|
||||
* @generated
|
||||
*/
|
||||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/sink/base');
|
||||
|
||||
|
||||
phutil_require_source('AphrontIsolatedHTTPSink.php');
|
|
@ -147,19 +147,13 @@ try {
|
|||
|
||||
$write_guard->dispose();
|
||||
|
||||
|
||||
$code = $response->getHTTPResponseCode();
|
||||
if ($code != 200) {
|
||||
header("HTTP/1.0 {$code}");
|
||||
}
|
||||
$sink = new AphrontPHPHTTPSink();
|
||||
$sink->writeHTTPStatus($response->getHTTPResponseCode());
|
||||
|
||||
$headers = $response->getCacheHeaders();
|
||||
$headers = array_merge($headers, $response->getHeaders());
|
||||
foreach ($headers as $header) {
|
||||
list($header, $value) = $header;
|
||||
header("{$header}: {$value}");
|
||||
}
|
||||
|
||||
$sink->writeHeaders($headers);
|
||||
|
||||
// TODO: This shouldn't be possible in a production-configured environment.
|
||||
if (isset($_REQUEST['__profile__']) &&
|
||||
|
@ -178,11 +172,11 @@ if (isset($_REQUEST['__profile__']) &&
|
|||
'<body>'.$profile,
|
||||
$response_string);
|
||||
} else {
|
||||
echo $profile;
|
||||
$sink->writeData($profile);
|
||||
}
|
||||
}
|
||||
|
||||
echo $response_string;
|
||||
$sink->writeData($response_string);
|
||||
|
||||
/**
|
||||
* @group aphront
|
||||
|
|
Loading…
Add table
Reference in a new issue