1
0
Fork 0
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:
epriestley 2012-02-14 14:51:51 -08:00
parent 8da4f981fb
commit c8b4bfdcd1
14 changed files with 143 additions and 16 deletions

View file

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

View file

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

View file

@ -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() {

View file

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

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

View 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');

View file

@ -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 )---------------------------------------------- */

View file

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

View file

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

View file

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

View file

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

View file

@ -56,8 +56,4 @@ class ConduitAPIResponse {
'error_info' => $this->getErrorInfo(), 'error_info' => $this->getErrorInfo(),
); );
} }
public function toJSON() {
return json_encode($this->toDictionary());
}
} }

View file

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

View file

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