mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 06:42:42 +01:00
Encode "<" and ">" in JSON/Ajax responses to prevent content-sniffing attacks
Summary: Some browsers will still sniff content types even with "Content-Type" and "X-Content-Type-Options: nosniff". Encode "<" and ">" to prevent them from sniffing the content as HTML. See T865. Also unified some of the code on this pathway. Test Plan: Verified Opera no longer sniffs the Conduit response into HTML for the test case in T865. Unit tests pass. Reviewers: cbg, btrahan Reviewed By: cbg CC: aran, epriestley Maniphest Tasks: T139, T865 Differential Revision: https://secure.phabricator.com/D1606
This commit is contained in:
parent
8da4f981fb
commit
c8b4bfdcd1
14 changed files with 143 additions and 16 deletions
|
@ -85,7 +85,7 @@ $response = id(new ConduitAPIResponse())
|
|||
->setResult($result)
|
||||
->setErrorCode($error_code)
|
||||
->setErrorInfo($error_info);
|
||||
echo $response->toJSON(), "\n";
|
||||
echo json_encode($response->toDictionary()), "\n";
|
||||
|
||||
// TODO -- how get $connection_id from SSH?
|
||||
$connection_id = null;
|
||||
|
|
|
@ -55,6 +55,7 @@ phutil_register_library_map(array(
|
|||
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
|
||||
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
|
||||
'AphrontJSONResponse' => 'aphront/response/json',
|
||||
'AphrontJavelinView' => 'view/javelin-view',
|
||||
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts',
|
||||
'AphrontListFilterView' => 'view/layout/listfilter',
|
||||
|
@ -880,6 +881,7 @@ phutil_register_library_map(array(
|
|||
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
||||
'AphrontJSONResponse' => 'AphrontResponse',
|
||||
'AphrontJavelinView' => 'AphrontView',
|
||||
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
||||
'AphrontListFilterView' => 'AphrontView',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* Copyright 2011 Facebook, Inc.
|
||||
* 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.
|
||||
|
@ -19,7 +19,7 @@
|
|||
/**
|
||||
* @group aphront
|
||||
*/
|
||||
class AphrontAjaxResponse extends AphrontResponse {
|
||||
final class AphrontAjaxResponse extends AphrontResponse {
|
||||
|
||||
private $content;
|
||||
private $error;
|
||||
|
@ -31,9 +31,13 @@ class AphrontAjaxResponse extends AphrontResponse {
|
|||
|
||||
public function buildResponseString() {
|
||||
$response = CelerityAPI::getStaticResourceResponse();
|
||||
return $response->renderAjaxResponse(
|
||||
$object = $response->buildAjaxResponse(
|
||||
$this->content,
|
||||
$this->error);
|
||||
|
||||
return $this->encodeJSONForHTTPResponse(
|
||||
$object,
|
||||
$use_javelin_shield = true);
|
||||
}
|
||||
|
||||
public function getHeaders() {
|
||||
|
|
|
@ -70,6 +70,37 @@ abstract class AphrontResponse {
|
|||
return $this;
|
||||
}
|
||||
|
||||
protected function encodeJSONForHTTPResponse(
|
||||
array $object,
|
||||
$use_javelin_shield) {
|
||||
|
||||
$response = json_encode($object);
|
||||
|
||||
// Prevent content sniffing attacks by encoding "<" and ">", so browsers
|
||||
// won't try to execute the document as HTML even if they ignore
|
||||
// Content-Type and X-Content-Type-Options. See T865.
|
||||
$response = str_replace(
|
||||
array('<', '>'),
|
||||
array('\u003c', '\u003e'),
|
||||
$response);
|
||||
|
||||
// Add a shield to prevent "JSON Hijacking" attacks where an attacker
|
||||
// requests a JSON response using a normal <script /> tag and then uses
|
||||
// Object.prototype.__defineSetter__() or similar to read response data.
|
||||
// This header causes the browser to loop infinitely instead of handing over
|
||||
// sensitive data.
|
||||
|
||||
// TODO: This is massively stupid: Javelin and Conduit use different
|
||||
// shields.
|
||||
$shield = $use_javelin_shield
|
||||
? 'for (;;);'
|
||||
: 'for(;;);';
|
||||
|
||||
$response = $shield.$response;
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getCacheHeaders() {
|
||||
$headers = array();
|
||||
if ($this->cacheable) {
|
||||
|
@ -94,7 +125,8 @@ abstract class AphrontResponse {
|
|||
// IE has a feature where it may override an explicit Content-Type
|
||||
// declaration by inferring a content type. This can be a security risk
|
||||
// and we always explicitly transmit the correct Content-Type header, so
|
||||
// prevent IE from using inferred content types.
|
||||
// prevent IE from using inferred content types. This only offers protection
|
||||
// on recent versions of IE; IE6/7 and Opera currently ignore this header.
|
||||
$headers[] = array('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
return $headers;
|
||||
|
|
46
src/aphront/response/json/AphrontJSONResponse.php
Normal file
46
src/aphront/response/json/AphrontJSONResponse.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @group aphront
|
||||
*/
|
||||
final class AphrontJSONResponse extends AphrontResponse {
|
||||
|
||||
private $content;
|
||||
|
||||
public function setContent($content) {
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function buildResponseString() {
|
||||
$response = $this->encodeJSONForHTTPResponse(
|
||||
$this->content,
|
||||
$use_javelin_shield = false);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getHeaders() {
|
||||
$headers = array(
|
||||
array('Content-Type', 'application/json'),
|
||||
);
|
||||
$headers = array_merge(parent::getHeaders(), $headers);
|
||||
return $headers;
|
||||
}
|
||||
|
||||
}
|
12
src/aphront/response/json/__init__.php
Normal file
12
src/aphront/response/json/__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/response/base');
|
||||
|
||||
|
||||
phutil_require_source('AphrontJSONResponse.php');
|
|
@ -105,6 +105,23 @@ abstract class AphrontHTTPSink {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write an entire @{class:AphrontResponse} to the output.
|
||||
*
|
||||
* @param AphrontResponse The response object to write.
|
||||
* @return void
|
||||
*/
|
||||
final public function writeResponse(AphrontResponse $response) {
|
||||
$all_headers = array_merge(
|
||||
$response->getHeaders(),
|
||||
$response->getCacheHeaders());
|
||||
|
||||
$this->writeHTTPStatus($response->getHTTPResponseCode());
|
||||
$this->writeHeaders($all_headers);
|
||||
$this->writeData($response->buildResponseString());
|
||||
}
|
||||
|
||||
|
||||
/* -( Emitting the Response )---------------------------------------------- */
|
||||
|
||||
|
||||
|
|
|
@ -79,4 +79,20 @@ final class AphrontHTTPSinkTestCase extends PhabricatorTestCase {
|
|||
$sink->writeHeaders(array(array($input, 'value')));
|
||||
}
|
||||
|
||||
public function testJSONContentSniff() {
|
||||
$response = id(new AphrontJSONResponse())
|
||||
->setContent(
|
||||
array(
|
||||
'x' => '<iframe>',
|
||||
));
|
||||
$sink = new AphrontIsolatedHTTPSink();
|
||||
$sink->writeResponse($response);
|
||||
|
||||
$this->assertEqual(
|
||||
'for(;;);{"x":"\u003ciframe\u003e"}',
|
||||
$sink->getEmittedData(),
|
||||
"JSONResponse should prevent content-sniffing attacks.");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/json');
|
||||
phutil_require_module('phabricator', 'aphront/sink/test');
|
||||
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
|
||||
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
phutil_require_source('AphrontHTTPSinkTestCase.php');
|
||||
|
|
|
@ -194,9 +194,8 @@ class PhabricatorConduitAPIController
|
|||
$response->toDictionary());
|
||||
case 'json':
|
||||
default:
|
||||
return id(new AphrontFileResponse())
|
||||
->setMimeType('application/json')
|
||||
->setContent('for(;;);'.$response->toJSON());
|
||||
return id(new AphrontJSONResponse())
|
||||
->setContent($response->toDictionary());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
|
||||
|
||||
phutil_require_module('phabricator', 'aphront/response/file');
|
||||
phutil_require_module('phabricator', 'aphront/response/json');
|
||||
phutil_require_module('phabricator', 'aphront/writeguard');
|
||||
phutil_require_module('phabricator', 'applications/conduit/controller/base');
|
||||
phutil_require_module('phabricator', 'applications/conduit/method/base');
|
||||
|
|
|
@ -56,8 +56,4 @@ class ConduitAPIResponse {
|
|||
'error_info' => $this->getErrorInfo(),
|
||||
);
|
||||
}
|
||||
|
||||
public function toJSON() {
|
||||
return json_encode($this->toDictionary());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ final class CelerityStaticResourceResponse {
|
|||
}
|
||||
}
|
||||
|
||||
public function renderAjaxResponse($payload, $error = null) {
|
||||
public function buildAjaxResponse($payload, $error = null) {
|
||||
$response = array(
|
||||
'error' => $error,
|
||||
'payload' => $payload,
|
||||
|
@ -205,9 +205,7 @@ final class CelerityStaticResourceResponse {
|
|||
$this->behaviors = array();
|
||||
}
|
||||
|
||||
$response = 'for (;;);'.json_encode($response);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -147,6 +147,8 @@ try {
|
|||
|
||||
$write_guard->dispose();
|
||||
|
||||
// TODO: Share the $sink->writeResponse() pathway here?
|
||||
|
||||
$sink = new AphrontPHPHTTPSink();
|
||||
$sink->writeHTTPStatus($response->getHTTPResponseCode());
|
||||
|
||||
|
|
Loading…
Reference in a new issue