1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-09 16:32:39 +01:00

Move ALL files to serve from the alternate file domain, not just files without

"Content-Disposition: attachment"

Summary:
We currently serve some files off the primary domain (with "Content-Disposition:
attachment" + a CSRF check) and some files off the alternate domain (without
either).

This is not sufficient, because some UAs (like the iPad) ignore
"Content-Disposition: attachment". So there's an attack that goes like this:

	- Alice uploads xss.html
	- Alice says to Bob "hey download this file on your iPad"
        - Bob clicks "Download" on Phabricator on his iPad, gets XSS'd.

NOTE: This removes the CSRF check for downloading files. The check is nice to
have but only raises the barrier to entry slightly. Between iPad / sniffing /
flash bytecode attacks, single-domain installs are simply insecure. We could
restore the check at some point in conjunction with a derived authentication
cookie (i.e., a mini-session-token which is only useful for downloading files),
but that's a lot of complexity to drop all at once.

(Because files are now authenticated only by knowing the PHID and secret key,
this also fixes the "no profile pictures in public feed while logged out"
issue.)

Test Plan: Viewed, info'd, and downloaded files

Reviewers: btrahan, arice, alok

Reviewed By: arice

CC: aran, epriestley

Maniphest Tasks: T843

Differential Revision: https://secure.phabricator.com/D1608
This commit is contained in:
epriestley 2012-02-14 14:52:27 -08:00
parent c8b4bfdcd1
commit 549146bc7c
10 changed files with 62 additions and 123 deletions

View file

@ -38,31 +38,20 @@ return array(
// -- IMPORTANT! Security! -------------------------------------------------- // // -- IMPORTANT! Security! -------------------------------------------------- //
// IMPORTANT: By default, Phabricator serves files from the same domain the // IMPORTANT: By default, Phabricator serves files from the same domain the
// application lives on. This is convenient but not secure: it creates // application lives on. This is convenient but not secure: it creates a large
// a vulnerability where an external attacker can: // class of vulnerabilities which can not be generally mitigated.
//
// - Convince a privileged user to upload a file which appears to be an
// image or some other inoccuous type of file (the file is actually both
// a JAR and an image); and
// - convince the user to give them the URI for the image; and
// - convince the user to click a link to a site which embeds the "image"
// using an <applet /> tag. This steals the user's credentials.
//
// If the attacker is internal, they can execute the first two steps
// themselves and need only convince another user to click a link in order to
// steal their credentials.
// //
// To avoid this, you should configure a second domain in the same way you // To avoid this, you should configure a second domain in the same way you
// have the primary domain configured (e.g., point it at the same machine and // have the primary domain configured (e.g., point it at the same machine and
// set up the same vhost rules) and provide it here. For instance, if your // set up the same vhost rules) and provide it here. For instance, if your
// primary install is on "http://www.phabricator-example.com/", you could // primary install is on "http://www.phabricator-example.com/", you could
// configure "http://www.phabricator-files.com/" and specify the entire // configure "http://www.phabricator-files.com/" and specify the entire
// domain (with protocol) here. This will enforce that viewable files are // domain (with protocol) here. This will enforce that files are
// served only from the alternate domain. Ideally, you should use a completely // served only from the alternate domain. Ideally, you should use a
// separate domain name rather than just a different subdomain. // completely separate domain name rather than just a different subdomain.
// //
// It is STRONGLY RECOMMENDED that you configure this. Phabricator makes this // It is STRONGLY RECOMMENDED that you configure this. Your install is NOT
// attack difficult, but it is viable unless you isolate the file domain. // SECURE unless you do so.
'security.alternate-file-domain' => null, 'security.alternate-file-domain' => null,
// Default key for HMAC digests where the key is not important (i.e., the // Default key for HMAC digests where the key is not important (i.e., the
@ -480,17 +469,15 @@ return array(
// Lists which uploaded file types may be viewed in the browser. If a file // Lists which uploaded file types may be viewed in the browser. If a file
// has a mime type which does not appear in this list, it will always be // has a mime type which does not appear in this list, it will always be
// downloaded instead of displayed. This is a security consideration: if a // downloaded instead of displayed. This is mainly a usability
// user uploads a file of type "text/html" and it is displayed as
// "text/html", they can easily execute XSS attacks. This is also a usability
// consideration, since browsers tend to freak out when viewing enormous // consideration, since browsers tend to freak out when viewing enormous
// binary files. // binary files.
// //
// The keys in this array are viewable mime types; the values are the mime // The keys in this array are viewable mime types; the values are the mime
// types they will be delivered as when they are viewed in the browser. // types they will be delivered as when they are viewed in the browser.
// //
// IMPORTANT: Making any file types viewable is a security vulnerability if // IMPORTANT: Configure 'security.alternate-file-domain' above! Your install
// you do not configure 'security.alternate-file-domain' above. // is NOT safe if it is left unconfigured.
'files.viewable-mime-types' => array( 'files.viewable-mime-types' => array(
'image/jpeg' => 'image/jpeg', 'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg', 'image/jpg' => 'image/jpg',

View file

@ -513,12 +513,13 @@ phutil_register_library_map(array(
'PhabricatorFeedStreamController' => 'applications/feed/controller/stream', 'PhabricatorFeedStreamController' => 'applications/feed/controller/stream',
'PhabricatorFeedView' => 'applications/feed/view/base', 'PhabricatorFeedView' => 'applications/feed/view/base',
'PhabricatorFile' => 'applications/files/storage/file', 'PhabricatorFile' => 'applications/files/storage/file',
'PhabricatorFileAltViewController' => 'applications/files/controller/altview',
'PhabricatorFileController' => 'applications/files/controller/base', 'PhabricatorFileController' => 'applications/files/controller/base',
'PhabricatorFileDAO' => 'applications/files/storage/base', 'PhabricatorFileDAO' => 'applications/files/storage/base',
'PhabricatorFileDataController' => 'applications/files/controller/data',
'PhabricatorFileDeleteController' => 'applications/files/controller/delete', 'PhabricatorFileDeleteController' => 'applications/files/controller/delete',
'PhabricatorFileDropUploadController' => 'applications/files/controller/dropupload', 'PhabricatorFileDropUploadController' => 'applications/files/controller/dropupload',
'PhabricatorFileImageMacro' => 'applications/files/storage/imagemacro', 'PhabricatorFileImageMacro' => 'applications/files/storage/imagemacro',
'PhabricatorFileInfoController' => 'applications/files/controller/info',
'PhabricatorFileListController' => 'applications/files/controller/list', 'PhabricatorFileListController' => 'applications/files/controller/list',
'PhabricatorFileMacroDeleteController' => 'applications/files/controller/macrodelete', 'PhabricatorFileMacroDeleteController' => 'applications/files/controller/macrodelete',
'PhabricatorFileMacroEditController' => 'applications/files/controller/macroedit', 'PhabricatorFileMacroEditController' => 'applications/files/controller/macroedit',
@ -533,7 +534,6 @@ phutil_register_library_map(array(
'PhabricatorFileUploadController' => 'applications/files/controller/upload', 'PhabricatorFileUploadController' => 'applications/files/controller/upload',
'PhabricatorFileUploadException' => 'applications/files/exception/upload', 'PhabricatorFileUploadException' => 'applications/files/exception/upload',
'PhabricatorFileUploadView' => 'applications/files/view/upload', 'PhabricatorFileUploadView' => 'applications/files/view/upload',
'PhabricatorFileViewController' => 'applications/files/controller/view',
'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/garbagecollector', 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/garbagecollector',
'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/goodfornothing', 'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/goodfornothing',
'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector',
@ -1240,12 +1240,13 @@ phutil_register_library_map(array(
'PhabricatorFeedStreamController' => 'PhabricatorFeedController', 'PhabricatorFeedStreamController' => 'PhabricatorFeedController',
'PhabricatorFeedView' => 'AphrontView', 'PhabricatorFeedView' => 'AphrontView',
'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFile' => 'PhabricatorFileDAO',
'PhabricatorFileAltViewController' => 'PhabricatorFileController',
'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileController' => 'PhabricatorController',
'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
'PhabricatorFileDataController' => 'PhabricatorFileController',
'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController',
'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController',
'PhabricatorFileImageMacro' => 'PhabricatorFileDAO', 'PhabricatorFileImageMacro' => 'PhabricatorFileDAO',
'PhabricatorFileInfoController' => 'PhabricatorFileController',
'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileListController' => 'PhabricatorFileController',
'PhabricatorFileMacroDeleteController' => 'PhabricatorFileController', 'PhabricatorFileMacroDeleteController' => 'PhabricatorFileController',
'PhabricatorFileMacroEditController' => 'PhabricatorFileController', 'PhabricatorFileMacroEditController' => 'PhabricatorFileController',
@ -1257,7 +1258,6 @@ phutil_register_library_map(array(
'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileTransformController' => 'PhabricatorFileController',
'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController',
'PhabricatorFileUploadView' => 'AphrontView', 'PhabricatorFileUploadView' => 'AphrontView',
'PhabricatorFileViewController' => 'PhabricatorFileController',
'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon',
'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker', 'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker',
'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpController' => 'PhabricatorController',

View file

@ -55,12 +55,15 @@ class AphrontDefaultApplicationConfiguration
'upload/$' => 'PhabricatorFileUploadController', 'upload/$' => 'PhabricatorFileUploadController',
'dropupload/$' => 'PhabricatorFileDropUploadController', 'dropupload/$' => 'PhabricatorFileDropUploadController',
'delete/(?P<id>\d+)/$' => 'PhabricatorFileDeleteController', 'delete/(?P<id>\d+)/$' => 'PhabricatorFileDeleteController',
'(?P<view>info)/(?P<phid>[^/]+)/' => 'PhabricatorFileViewController', 'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'(?P<view>view)/(?P<phid>[^/]+)/' => 'PhabricatorFileViewController',
'(?P<view>download)/(?P<phid>[^/]+)/' 'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/'
=> 'PhabricatorFileViewController', => 'PhabricatorFileDataController',
// TODO: This is a deprecated version of /data/. Remove it after
// old links have had a chance to rot.
'alt/(?P<key>[^/]+)/(?P<phid>[^/]+)/' 'alt/(?P<key>[^/]+)/(?P<phid>[^/]+)/'
=> 'PhabricatorFileAltViewController', => 'PhabricatorFileDataController',
'macro/' => array( 'macro/' => array(
'$' => 'PhabricatorFileMacroListController', '$' => 'PhabricatorFileMacroListController',
'edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorFileMacroEditController', 'edit/(?:(?P<id>\d+)/)?$' => 'PhabricatorFileMacroEditController',

View file

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
class PhabricatorFileAltViewController extends PhabricatorFileController { final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid; private $phid;
private $key; private $key;
@ -31,16 +31,11 @@ class PhabricatorFileAltViewController extends PhabricatorFileController {
} }
public function processRequest() { public function processRequest() {
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
return new Aphront400Response();
}
$request = $this->getRequest(); $request = $this->getRequest();
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$alt_domain = id(new PhutilURI($alt))->getDomain(); $alt_domain = id(new PhutilURI($alt))->getDomain();
if ($alt_domain != $request->getHost()) { if ($alt_domain && ($alt_domain != $request->getHost())) {
return new Aphront400Response(); return new Aphront400Response();
} }
@ -55,15 +50,29 @@ class PhabricatorFileAltViewController extends PhabricatorFileController {
return new Aphront403Response(); return new Aphront403Response();
} }
// It's safe to bypass view restrictions because we know we are being served
// off an alternate domain which we will not set cookies on.
$data = $file->loadFileData(); $data = $file->loadFileData();
$response = new AphrontFileResponse(); $response = new AphrontFileResponse();
$response->setContent($data); $response->setContent($data);
$response->setMimeType($file->getMimeType());
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
$is_view = $file->isViewableInBrowser();
if ($is_view) {
$response->setMimeType($file->getViewableMimeType());
} else {
if (!$request->isHTTPPost()) {
// NOTE: Require POST to download files. We'd rather go full-bore and
// do a real CSRF check, but can't currently authenticate users on the
// file domain. This should blunt any attacks based on iframes, script
// tags, applet tags, etc., at least. Send the user to the "info" page
// if they're using some other method.
return id(new AphrontRedirectResponse())
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
}
$response->setMimeType($file->getMimeType());
$response->setDownload($file->getName());
}
return $response; return $response;
} }
} }

View file

@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/403'); phutil_require_module('phabricator', 'aphront/response/403');
phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/file'); phutil_require_module('phabricator', 'aphront/response/file');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/files/controller/base'); phutil_require_module('phabricator', 'applications/files/controller/base');
phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'infrastructure/env');
@ -18,4 +19,4 @@ phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils'); phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorFileAltViewController.php'); phutil_require_source('PhabricatorFileDataController.php');

View file

@ -16,14 +16,12 @@
* limitations under the License. * limitations under the License.
*/ */
class PhabricatorFileViewController extends PhabricatorFileController { final class PhabricatorFileInfoController extends PhabricatorFileController {
private $phid; private $phid;
private $view;
public function willProcessRequest(array $data) { public function willProcessRequest(array $data) {
$this->phid = $data['phid']; $this->phid = $data['phid'];
$this->view = $data['view'];
} }
public function processRequest() { public function processRequest() {
@ -38,61 +36,6 @@ class PhabricatorFileViewController extends PhabricatorFileController {
return new Aphront404Response(); return new Aphront404Response();
} }
switch ($this->view) {
case 'download':
case 'view':
$data = $file->loadFileData();
$response = new AphrontFileResponse();
$response->setContent($data);
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
if ($this->view == 'view') {
if (!$file->isViewableInBrowser()) {
return new Aphront400Response();
}
$download = false;
} else {
$download = true;
}
if ($download) {
if (!$request->isFormPost()) {
// Require a POST to download files to hinder attacks where you
// <applet src="http://phabricator.example.com/file/..." /> on some
// other domain.
return id(new AphrontRedirectResponse())
->setURI($file->getInfoURI());
}
}
if ($download) {
$mime_type = $file->getMimeType();
} else {
$mime_type = $file->getViewableMimeType();
}
// If an alternate file domain is configured, forbid all views which
// don't originate from it.
if (!$download) {
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
if ($alt) {
$domain = id(new PhutilURI($alt))->getDomain();
if ($domain != $request->getHost()) {
return new Aphront400Response();
}
}
}
$response->setMimeType($mime_type);
if ($download) {
$response->setDownload($file->getName());
}
return $response;
default:
break;
}
$author_child = null; $author_child = null;
if ($file->getAuthorPHID()) { if ($file->getAuthorPHID()) {
$author = id(new PhabricatorUser())->loadOneWhere( $author = id(new PhabricatorUser())->loadOneWhere(
@ -111,11 +54,10 @@ class PhabricatorFileViewController extends PhabricatorFileController {
$submit = new AphrontFormSubmitControl(); $submit = new AphrontFormSubmitControl();
$form->setAction($file->getViewURI());
if ($file->isViewableInBrowser()) { if ($file->isViewableInBrowser()) {
$form->setAction($file->getViewURI());
$submit->setValue('View File'); $submit->setValue('View File');
} else { } else {
$form->setAction('/file/download/'.$file->getPHID().'/');
$submit->setValue('Download File'); $submit->setValue('Download File');
} }

View file

@ -6,15 +6,11 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/404'); phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/file');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/files/controller/base'); phutil_require_module('phabricator', 'applications/files/controller/base');
phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/files/storage/transformed'); phutil_require_module('phabricator', 'applications/files/storage/transformed');
phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/static'); phutil_require_module('phabricator', 'view/form/control/static');
@ -23,8 +19,7 @@ phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/utils'); phutil_require_module('phabricator', 'view/utils');
phutil_require_module('phutil', 'markup'); phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils'); phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorFileViewController.php'); phutil_require_source('PhabricatorFileInfoController.php');

View file

@ -230,16 +230,8 @@ class PhabricatorFile extends PhabricatorFileDAO {
$name = phutil_escape_uri($this->getName()); $name = phutil_escape_uri($this->getName());
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
if ($alt) { return PhabricatorEnv::getCDNURI($path);
$path = '/file/alt/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name;
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
} else {
return '/file/view/'.$this->getPHID().'/'.$name;
}
} }
public function getInfoURI() { public function getInfoURI() {

View file

@ -46,6 +46,16 @@ final class PhabricatorEnv {
return rtrim($uri, '/').$path; return rtrim($uri, '/').$path;
} }
public static function getCDNURI($path) {
$alt = self::getEnvConfig('security.alternate-file-domain');
if (!$alt) {
$alt = self::getEnvConfig('phabricator.base-uri');
}
$uri = new PhutilURI($alt);
$uri->setPath($path);
return (string)$uri;
}
public static function getAllConfigKeys() { public static function getAllConfigKeys() {
return self::$env; return self::$env;
} }

View file

@ -117,8 +117,8 @@ class PhabricatorSetup {
if (!PhabricatorEnv::getEnvConfig('security.alternate-file-domain')) { if (!PhabricatorEnv::getEnvConfig('security.alternate-file-domain')) {
self::write( self::write(
"[WARN] You have not configured 'security.alternate-file-domain'. ". "[WARN] You have not configured 'security.alternate-file-domain'. ".
"This may make your installation vulnerable to attack. Make sure ". "This makes your installation vulnerable to attack. Make sure you ".
"you read the documentation for this parameter and understand the ". "read the documentation for this parameter and understand the ".
"consequences of leaving it unconfigured.\n"); "consequences of leaving it unconfigured.\n");
} }