diff --git a/scripts/conduit/api.php b/scripts/conduit/api.php
index 2844c2f393..e2da82ce6e 100644
--- a/scripts/conduit/api.php
+++ b/scripts/conduit/api.php
@@ -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;
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index fb58757510..77037eed33 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -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',
diff --git a/src/aphront/response/ajax/AphrontAjaxResponse.php b/src/aphront/response/ajax/AphrontAjaxResponse.php
index b64d053f17..a22cbc6eeb 100644
--- a/src/aphront/response/ajax/AphrontAjaxResponse.php
+++ b/src/aphront/response/ajax/AphrontAjaxResponse.php
@@ -1,7 +1,7 @@
renderAjaxResponse(
+ $object = $response->buildAjaxResponse(
$this->content,
$this->error);
+
+ return $this->encodeJSONForHTTPResponse(
+ $object,
+ $use_javelin_shield = true);
}
public function getHeaders() {
diff --git a/src/aphront/response/base/AphrontResponse.php b/src/aphront/response/base/AphrontResponse.php
index c4b4ce8b31..6c32b7f744 100644
--- a/src/aphront/response/base/AphrontResponse.php
+++ b/src/aphront/response/base/AphrontResponse.php
@@ -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 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;
diff --git a/src/aphront/response/json/AphrontJSONResponse.php b/src/aphront/response/json/AphrontJSONResponse.php
new file mode 100644
index 0000000000..456fae4eaa
--- /dev/null
+++ b/src/aphront/response/json/AphrontJSONResponse.php
@@ -0,0 +1,46 @@
+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;
+ }
+
+}
diff --git a/src/aphront/response/json/__init__.php b/src/aphront/response/json/__init__.php
new file mode 100644
index 0000000000..d6d6e8e97a
--- /dev/null
+++ b/src/aphront/response/json/__init__.php
@@ -0,0 +1,12 @@
+getHeaders(),
+ $response->getCacheHeaders());
+
+ $this->writeHTTPStatus($response->getHTTPResponseCode());
+ $this->writeHeaders($all_headers);
+ $this->writeData($response->buildResponseString());
+ }
+
+
/* -( Emitting the Response )---------------------------------------------- */
diff --git a/src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php b/src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php
index 7d6e7edc05..3aa09ce72b 100644
--- a/src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php
+++ b/src/aphront/sink/base/__tests__/AphrontHTTPSinkTestCase.php
@@ -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' => '