mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-26 00:32: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)
|
->setResult($result)
|
||||||
->setErrorCode($error_code)
|
->setErrorCode($error_code)
|
||||||
->setErrorInfo($error_info);
|
->setErrorInfo($error_info);
|
||||||
echo $response->toJSON(), "\n";
|
echo json_encode($response->toDictionary()), "\n";
|
||||||
|
|
||||||
// TODO -- how get $connection_id from SSH?
|
// TODO -- how get $connection_id from SSH?
|
||||||
$connection_id = null;
|
$connection_id = null;
|
||||||
|
|
|
@ -55,6 +55,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
|
'AphrontIsolatedDatabaseConnection' => 'storage/connection/isolated',
|
||||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
|
'AphrontIsolatedDatabaseConnectionTestCase' => 'storage/connection/isolated/__tests__',
|
||||||
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
|
'AphrontIsolatedHTTPSink' => 'aphront/sink/test',
|
||||||
|
'AphrontJSONResponse' => 'aphront/response/json',
|
||||||
'AphrontJavelinView' => 'view/javelin-view',
|
'AphrontJavelinView' => 'view/javelin-view',
|
||||||
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts',
|
'AphrontKeyboardShortcutsAvailableView' => 'view/widget/keyboardshortcuts',
|
||||||
'AphrontListFilterView' => 'view/layout/listfilter',
|
'AphrontListFilterView' => 'view/layout/listfilter',
|
||||||
|
@ -880,6 +881,7 @@ phutil_register_library_map(array(
|
||||||
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
|
||||||
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase',
|
||||||
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink',
|
||||||
|
'AphrontJSONResponse' => 'AphrontResponse',
|
||||||
'AphrontJavelinView' => 'AphrontView',
|
'AphrontJavelinView' => 'AphrontView',
|
||||||
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
'AphrontKeyboardShortcutsAvailableView' => 'AphrontView',
|
||||||
'AphrontListFilterView' => 'AphrontView',
|
'AphrontListFilterView' => 'AphrontView',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright 2011 Facebook, Inc.
|
* Copyright 2012 Facebook, Inc.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
/**
|
/**
|
||||||
* @group aphront
|
* @group aphront
|
||||||
*/
|
*/
|
||||||
class AphrontAjaxResponse extends AphrontResponse {
|
final class AphrontAjaxResponse extends AphrontResponse {
|
||||||
|
|
||||||
private $content;
|
private $content;
|
||||||
private $error;
|
private $error;
|
||||||
|
@ -31,9 +31,13 @@ class AphrontAjaxResponse extends AphrontResponse {
|
||||||
|
|
||||||
public function buildResponseString() {
|
public function buildResponseString() {
|
||||||
$response = CelerityAPI::getStaticResourceResponse();
|
$response = CelerityAPI::getStaticResourceResponse();
|
||||||
return $response->renderAjaxResponse(
|
$object = $response->buildAjaxResponse(
|
||||||
$this->content,
|
$this->content,
|
||||||
$this->error);
|
$this->error);
|
||||||
|
|
||||||
|
return $this->encodeJSONForHTTPResponse(
|
||||||
|
$object,
|
||||||
|
$use_javelin_shield = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getHeaders() {
|
public function getHeaders() {
|
||||||
|
|
|
@ -70,6 +70,37 @@ abstract class AphrontResponse {
|
||||||
return $this;
|
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() {
|
public function getCacheHeaders() {
|
||||||
$headers = array();
|
$headers = array();
|
||||||
if ($this->cacheable) {
|
if ($this->cacheable) {
|
||||||
|
@ -94,7 +125,8 @@ abstract class AphrontResponse {
|
||||||
// IE has a feature where it may override an explicit Content-Type
|
// IE has a feature where it may override an explicit Content-Type
|
||||||
// declaration by inferring a content type. This can be a security risk
|
// declaration by inferring a content type. This can be a security risk
|
||||||
// and we always explicitly transmit the correct Content-Type header, so
|
// 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');
|
$headers[] = array('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
return $headers;
|
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 )---------------------------------------------- */
|
/* -( Emitting the Response )---------------------------------------------- */
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -79,4 +79,20 @@ final class AphrontHTTPSinkTestCase extends PhabricatorTestCase {
|
||||||
$sink->writeHeaders(array(array($input, 'value')));
|
$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', 'aphront/sink/test');
|
||||||
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
|
phutil_require_module('phabricator', 'infrastructure/testing/testcase');
|
||||||
|
|
||||||
|
phutil_require_module('phutil', 'utils');
|
||||||
|
|
||||||
|
|
||||||
phutil_require_source('AphrontHTTPSinkTestCase.php');
|
phutil_require_source('AphrontHTTPSinkTestCase.php');
|
||||||
|
|
|
@ -194,9 +194,8 @@ class PhabricatorConduitAPIController
|
||||||
$response->toDictionary());
|
$response->toDictionary());
|
||||||
case 'json':
|
case 'json':
|
||||||
default:
|
default:
|
||||||
return id(new AphrontFileResponse())
|
return id(new AphrontJSONResponse())
|
||||||
->setMimeType('application/json')
|
->setContent($response->toDictionary());
|
||||||
->setContent('for(;;);'.$response->toJSON());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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', 'aphront/writeguard');
|
||||||
phutil_require_module('phabricator', 'applications/conduit/controller/base');
|
phutil_require_module('phabricator', 'applications/conduit/controller/base');
|
||||||
phutil_require_module('phabricator', 'applications/conduit/method/base');
|
phutil_require_module('phabricator', 'applications/conduit/method/base');
|
||||||
|
|
|
@ -56,8 +56,4 @@ class ConduitAPIResponse {
|
||||||
'error_info' => $this->getErrorInfo(),
|
'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(
|
$response = array(
|
||||||
'error' => $error,
|
'error' => $error,
|
||||||
'payload' => $payload,
|
'payload' => $payload,
|
||||||
|
@ -205,9 +205,7 @@ final class CelerityStaticResourceResponse {
|
||||||
$this->behaviors = array();
|
$this->behaviors = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = 'for (;;);'.json_encode($response);
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,8 @@ try {
|
||||||
|
|
||||||
$write_guard->dispose();
|
$write_guard->dispose();
|
||||||
|
|
||||||
|
// TODO: Share the $sink->writeResponse() pathway here?
|
||||||
|
|
||||||
$sink = new AphrontPHPHTTPSink();
|
$sink = new AphrontPHPHTTPSink();
|
||||||
$sink->writeHTTPStatus($response->getHTTPResponseCode());
|
$sink->writeHTTPStatus($response->getHTTPResponseCode());
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue