1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 00:10:57 +01:00

Protect file data with a one-time-token

Test Plan: currently untested work in progress

Reviewers: #blessed_reviewers, epriestley

Subscribers: rush898, aklapper, Korvin, epriestley

Projects: #wikimedia

Maniphest Tasks: T5685

Differential Revision: https://secure.phabricator.com/D10054
This commit is contained in:
Mukunda Modell 2014-08-11 07:08:58 -07:00 committed by epriestley
parent c0919be0ec
commit 25ae4c458d
4 changed files with 129 additions and 27 deletions

View file

@ -0,0 +1,20 @@
<?php
$table = new PhabricatorFile();
$conn_w = $table->establishConnection('w');
foreach (new LiskMigrationIterator($table) as $file) {
$id = $file->getID();
echo "Updating flags for file {$id}...\n";
$meta = $file->getMetadata();
if (!idx($meta, 'canCDN')) {
$meta['canCDN'] = true;
queryfx(
$conn_w,
'UPDATE %T SET metadata = %s WHERE id = %d',
$table->getTableName(),
json_encode($meta),
$id);
}
}

View file

@ -52,6 +52,8 @@ final class PhabricatorFilesApplication extends PhabricatorApplication {
'delete/(?P<id>[1-9]\d*)/' => 'PhabricatorFileDeleteController',
'edit/(?P<id>[1-9]\d*)/' => 'PhabricatorFileEditController',
'info/(?P<phid>[^/]+)/' => 'PhabricatorFileInfoController',
'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/(?P<token>[^/]+)/.*'
=> 'PhabricatorFileDataController',
'data/(?P<key>[^/]+)/(?P<phid>[^/]+)/.*'
=> 'PhabricatorFileDataController',
'proxy/' => 'PhabricatorFileProxyController',

View file

@ -4,35 +4,19 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
private $phid;
private $key;
private $token;
public function willProcessRequest(array $data) {
$this->phid = $data['phid'];
$this->key = $data['key'];
$this->token = idx($data, 'token');
}
public function shouldRequireLogin() {
return false;
}
public function processRequest() {
$request = $this->getRequest();
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$uri = new PhutilURI($alt);
$alt_domain = $uri->getDomain();
if ($alt_domain && ($alt_domain != $request->getHost())) {
return id(new AphrontRedirectResponse())
->setURI($uri->setPath($request->getPath()));
}
// NOTE: This endpoint will ideally be accessed via CDN or otherwise on
// a non-credentialed domain. Knowing the file's secret key gives you
// access, regardless of authentication on the request itself.
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->phid))
->executeOne();
protected function checkFileAndToken($file) {
if (!$file) {
return new Aphront404Response();
}
@ -41,10 +25,97 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
return new Aphront403Response();
}
return null;
}
public function processRequest() {
$request = $this->getRequest();
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
$alt_uri = new PhutilURI($alt);
$alt_domain = $alt_uri->getDomain();
$req_domain = $request->getHost();
$main_domain = id(new PhutilURI($base_uri))->getDomain();
$cache_response = true;
if (empty($alt) || $main_domain == $alt_domain) {
// Alternate files domain isn't configured or it's set
// to the same as the default domain
// load the file with permissions checks;
$file = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withPHIDs(array($this->phid))
->executeOne();
$error_response = $this->checkFileAndToken($file);
if ($error_response) {
return $error_response;
}
// when the file is not CDNable, don't allow cache
$cache_response = $file->getCanCDN();
} else if ($req_domain != $alt_domain) {
// Alternate domain is configured but this request isn't using it
// load the file with permissions checks;
$file = id(new PhabricatorFileQuery())
->setViewer($request->getUser())
->withPHIDs(array($this->phid))
->executeOne();
$error_response = $this->checkFileAndToken($file);
if ($error_response) {
return $error_response;
}
// if the user can see the file, generate a token;
// redirect to the alt domain with the token;
return id(new AphrontRedirectResponse())
->setURI($file->getCDNURIWithToken());
} else {
// We are using the alternate domain
// load the file, bypassing permission checks;
$file = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($this->phid))
->executeOne();
$error_response = $this->checkFileAndToken($file);
if ($error_response) {
return $error_response;
}
if ($this->token) {
// validate the token, if it is valid, continue
$validated_token = $file->validateOneTimeToken($this->token);
if (!$validated_token) {
return new Aphront403Response();
}
// return the file data without cache headers
$cache_response = false;
} else if (!$file->getCanCDN()) {
// file cannot be served via cdn, and no token given
// redirect to the main domain to aquire a token
$file_uri = id(new PhutilURI($file->getViewURI()))
->setDomain($main_domain);
return id(new AphrontRedirectResponse())
->setURI($file_uri);
}
}
$data = $file->loadFileData();
$response = new AphrontFileResponse();
$response->setContent($data);
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
if ($cache_response) {
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
}
// NOTE: It's important to accept "Range" requests when playing audio.
// If we don't, Safari has difficulty figuring out how long sounds are
@ -58,6 +129,11 @@ final class PhabricatorFileDataController extends PhabricatorFileController {
$response->setHTTPResponseCode(206);
$response->setRange((int)$matches[1], (int)$matches[2]);
}
} else if (isset($validated_token)) {
// consume the one-time token if we have one.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$validated_token->delete();
unset($unguarded);
}
$is_viewable = $file->isViewableInBrowser();

View file

@ -873,12 +873,16 @@ final class PhabricatorFile extends PhabricatorFileDAO
$key = Filesystem::readRandomCharacters(16);
// Save the new secret.
return id(new PhabricatorAuthTemporaryToken())
->setObjectPHID($this->getPHID())
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->save();
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$token = id(new PhabricatorAuthTemporaryToken())
->setObjectPHID($this->getPHID())
->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE)
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
->setTokenCode(PhabricatorHash::digest($key))
->save();
unset($unguarded);
return $key;
}
public function validateOneTimeToken($token_code) {
@ -887,7 +891,7 @@ final class PhabricatorFile extends PhabricatorFileDAO
->withObjectPHIDs(array($this->getPHID()))
->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE))
->withExpired(false)
->withTokenCodes(array($token_code))
->withTokenCodes(array(PhabricatorHash::digest($token_code)))
->executeOne();
return $token;