mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-12 08:36:13 +01:00
dba4c4bdf6
Summary: See PHI399. Ref T4340. This header provides an additional layer of protection against various attacks, including XSS attacks which embed inline `<script ...>` or `onhover="..."` content into the document. **style-src**: The "unsafe-inline" directive affects both `style="..."` and `<style>`. We use a lot of `style="..."`, some very legitimately, so we can't realistically get away from this any time soon. We only use one `<style>` (for monospaced font preferences) but can't disable `<style>` without disabling `style="..."`. **img-src**: We use "data:" URIs to inline small images into CSS, and there's a significant performance benefit from doing this. There doesn't seem to be a way to allow "data" URIs in CSS without allowing them in the document itself. **script-src** and **frame-src**: For a small number of flows (Recaptcha, Stripe) we embed external javascript, some of which embeds child elements (or additional resources) into the document. We now whitelist these narrowly on the respective pages. This won't work with Quicksand, so I've blacklisted it for now. **connect-src**: We need to include `'self'` for AJAX to work, and any websocket URIs. **Clickjacking**: We now have three layers of protection: - X-Frame-Options: works in older browsers. - `frame-ancestors 'none'`: does the same thing. - Explicit framebust in JX.Stratcom after initialization: works in ancient IE. We could probably drop the explicit framebust but it wasn't difficult to retain. **script tags**: We previously used an inline `<script>` tag to start Javelin. I've moved this to `<data data-javelin-init ...>` tags, which seems to work properly. **`__DEV__`**: We previously used an inline `<script>` tag to set the `__DEV__` mode flag. I tried using the "initialization" tags for this, but they fire too late. I moved it to `<html data-developer-mode="1">`, which seems OK everywhere. **CSP Scope**: Only the CSP header on the original request appears to matter -- you can't refine the scope by emitting headers on CSS/JS. To reduce confusion, I disabled the headers on those response types. More headers could be disabled, although we're likely already deep in the land of diminishing returns. **Initialization**: The initialization sequence has changed slightly. Previously, we waited for the <script> in bottom of the document to evaluate. Now, we go fishing for tags when domcontentready fires. Test Plan: - Browsed around in Firefox, Safari and Chrome looking for console warnings. Interacted with various Javascript behaviors. Enabled Quicksand. - Disabled all the framebusting, launched a clickjacking attack, verified that each layer of protection is individually effective. - Verified that the XHProf iframe in Darkconsole and the PHPAST frame layout work properly. - Enabled notifications, verified no complaints about connecting to Aphlict. - Hit `__DEV__` mode warnings based on the new data attribute. - Tried to do sketchy stuff with `data:` URIs and SVGs. This works but doesn't seem to be able to do anything dangerous. - Went through the Stripe and Recaptcha workflows. - Dumped and examined the CSP headers with `curl`, etc. - Added a raw <script> tag to a page (as though I'd found an XSS attack), verified it was no longer executed. Maniphest Tasks: T4340 Differential Revision: https://secure.phabricator.com/D19143
212 lines
6 KiB
PHP
212 lines
6 KiB
PHP
<?php
|
|
|
|
abstract class CelerityResourceController extends PhabricatorController {
|
|
|
|
protected function buildResourceTransformer() {
|
|
return null;
|
|
}
|
|
|
|
public function shouldRequireLogin() {
|
|
return false;
|
|
}
|
|
|
|
public function shouldRequireEnabledUser() {
|
|
return false;
|
|
}
|
|
|
|
public function shouldAllowPartialSessions() {
|
|
return true;
|
|
}
|
|
|
|
public function shouldAllowLegallyNonCompliantUsers() {
|
|
return true;
|
|
}
|
|
|
|
abstract public function getCelerityResourceMap();
|
|
|
|
protected function serveResource(array $spec) {
|
|
$path = $spec['path'];
|
|
$hash = idx($spec, 'hash');
|
|
|
|
// Sanity checking to keep this from exposing anything sensitive, since it
|
|
// ultimately boils down to disk reads.
|
|
if (preg_match('@(//|\.\.)@', $path)) {
|
|
return new Aphront400Response();
|
|
}
|
|
|
|
$type = CelerityResourceTransformer::getResourceType($path);
|
|
$type_map = self::getSupportedResourceTypes();
|
|
|
|
if (empty($type_map[$type])) {
|
|
throw new Exception(pht('Only static resources may be served.'));
|
|
}
|
|
|
|
$dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
|
|
|
|
$map = $this->getCelerityResourceMap();
|
|
$expect_hash = $map->getHashForName($path);
|
|
|
|
// Test if the URI hash is correct for our current resource map. If it
|
|
// is not, refuse to cache this resource. This avoids poisoning caches
|
|
// and CDNs if we're getting a request for a new resource to an old node
|
|
// shortly after a push.
|
|
$is_cacheable = ($hash === $expect_hash);
|
|
$is_locally_cacheable = $this->isLocallyCacheableResourceType($type);
|
|
if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
|
|
// Return a "304 Not Modified". We don't care about the value of this
|
|
// field since we never change what resource is served by a given URI.
|
|
return $this->makeResponseCacheable(new Aphront304Response());
|
|
}
|
|
|
|
$cache = null;
|
|
$data = null;
|
|
if ($is_cacheable && $is_locally_cacheable && !$dev_mode) {
|
|
$cache = PhabricatorCaches::getImmutableCache();
|
|
|
|
$request_path = $this->getRequest()->getPath();
|
|
$cache_key = $this->getCacheKey($request_path);
|
|
|
|
$data = $cache->getKey($cache_key);
|
|
}
|
|
|
|
if ($data === null) {
|
|
if ($map->isPackageResource($path)) {
|
|
$resource_names = $map->getResourceNamesForPackageName($path);
|
|
if (!$resource_names) {
|
|
return new Aphront404Response();
|
|
}
|
|
|
|
try {
|
|
$data = array();
|
|
foreach ($resource_names as $resource_name) {
|
|
$data[] = $map->getResourceDataForName($resource_name);
|
|
}
|
|
$data = implode("\n\n", $data);
|
|
} catch (Exception $ex) {
|
|
return new Aphront404Response();
|
|
}
|
|
} else {
|
|
try {
|
|
$data = $map->getResourceDataForName($path);
|
|
} catch (Exception $ex) {
|
|
return new Aphront404Response();
|
|
}
|
|
}
|
|
|
|
$xformer = $this->buildResourceTransformer();
|
|
if ($xformer) {
|
|
$data = $xformer->transformResource($path, $data);
|
|
}
|
|
|
|
if ($cache) {
|
|
$cache->setKey($cache_key, $data);
|
|
}
|
|
}
|
|
|
|
$response = id(new AphrontFileResponse())
|
|
->setMimeType($type_map[$type]);
|
|
|
|
// The "Content-Security-Policy" header has no effect on the actual
|
|
// resources, only on the main request. Disable it on the resource
|
|
// responses to limit confusion.
|
|
$response->setDisableContentSecurityPolicy(true);
|
|
|
|
$range = AphrontRequest::getHTTPHeader('Range');
|
|
|
|
if (strlen($range)) {
|
|
$response->setContentLength(strlen($data));
|
|
|
|
list($range_begin, $range_end) = $response->parseHTTPRange($range);
|
|
|
|
if ($range_begin !== null) {
|
|
if ($range_end !== null) {
|
|
$data = substr($data, $range_begin, ($range_end - $range_begin));
|
|
} else {
|
|
$data = substr($data, $range_begin);
|
|
}
|
|
}
|
|
|
|
$response->setContentIterator(array($data));
|
|
} else {
|
|
$response
|
|
->setContent($data)
|
|
->setCompressResponse(true);
|
|
}
|
|
|
|
|
|
// NOTE: This is a piece of magic required to make WOFF fonts work in
|
|
// Firefox and IE. Possibly we should generalize this more.
|
|
|
|
$cross_origin_types = array(
|
|
'woff' => true,
|
|
'woff2' => true,
|
|
'eot' => true,
|
|
);
|
|
|
|
if (isset($cross_origin_types[$type])) {
|
|
// We could be more tailored here, but it's not currently trivial to
|
|
// generate a comprehensive list of valid origins (an install may have
|
|
// arbitrarily many Phame blogs, for example), and we lose nothing by
|
|
// allowing access from anywhere.
|
|
$response->addAllowOrigin('*');
|
|
}
|
|
|
|
if ($is_cacheable) {
|
|
$response = $this->makeResponseCacheable($response);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
public static function getSupportedResourceTypes() {
|
|
return array(
|
|
'css' => 'text/css; charset=utf-8',
|
|
'js' => 'text/javascript; charset=utf-8',
|
|
'png' => 'image/png',
|
|
'svg' => 'image/svg+xml',
|
|
'gif' => 'image/gif',
|
|
'jpg' => 'image/jpeg',
|
|
'swf' => 'application/x-shockwave-flash',
|
|
'woff' => 'font/woff',
|
|
'woff2' => 'font/woff2',
|
|
'eot' => 'font/eot',
|
|
'ttf' => 'font/ttf',
|
|
'mp3' => 'audio/mpeg',
|
|
'ico' => 'image/x-icon',
|
|
);
|
|
}
|
|
|
|
private function makeResponseCacheable(AphrontResponse $response) {
|
|
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
|
|
$response->setLastModified(time());
|
|
$response->setCanCDN(true);
|
|
|
|
return $response;
|
|
}
|
|
|
|
|
|
/**
|
|
* Is it appropriate to cache the data for this resource type in the fast
|
|
* immutable cache?
|
|
*
|
|
* Generally, text resources (which are small, and expensive to process)
|
|
* are cached, while other types of resources (which are large, and cheap
|
|
* to process) are not.
|
|
*
|
|
* @param string Resource type.
|
|
* @return bool True to enable caching.
|
|
*/
|
|
private function isLocallyCacheableResourceType($type) {
|
|
$types = array(
|
|
'js' => true,
|
|
'css' => true,
|
|
);
|
|
|
|
return isset($types[$type]);
|
|
}
|
|
|
|
protected function getCacheKey($path) {
|
|
return 'celerity:'.PhabricatorHash::digestToLength($path, 64);
|
|
}
|
|
|
|
}
|