Skip to content

Commit ab579f2

Browse files
author
epriestley
committedMar 1, 2018
Never generate file download forms which point to the CDN domain, tighten "form-action" CSP
Summary: Depends on D19155. Ref T13094. Ref T4340. We can't currently implement a strict `form-action 'self'` content security policy because some file downloads rely on a `<form />` which sometimes POSTs to the CDN domain. Broadly, stop generating these forms. We just redirect instead, and show an interstitial confirm dialog if no CDN domain is configured. This makes the UX for installs with no CDN domain a little worse and the UX for everyone else better. Then, implement the stricter Content-Security-Policy. This also removes extra confirm dialogs for downloading Harbormaster build logs and data exports. Test Plan: - Went through the plain data export, data export with bulk jobs, ssh key generation, calendar ICS download, Diffusion data, Paste data, Harbormaster log data, and normal file data download workflows with a CDN domain. - Went through all those workflows again without a CDN domain. - Grepped for affected symbols (`getCDNURI()`, `getDownloadURI()`). - Added an evil form to a page, tried to submit it, was rejected. - Went through the ReCaptcha and Stripe flows again to see if they're submitting any forms. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13094, T4340 Differential Revision: https://secure.phabricator.com/D19156
1 parent afc98f5 commit ab579f2

16 files changed

+118
-82
lines changed
 

‎resources/celerity/map.php

+14-14
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
'conpherence.pkg.css' => 'e68cf1fa',
1111
'conpherence.pkg.js' => '15191c65',
1212
'core.pkg.css' => '2fa91e14',
13-
'core.pkg.js' => '7aa5bd92',
13+
'core.pkg.js' => 'e7ce7bba',
1414
'darkconsole.pkg.js' => '1f9a31bc',
1515
'differential.pkg.css' => '113e692c',
1616
'differential.pkg.js' => 'f6d809c0',
@@ -255,7 +255,7 @@
255255
'rsrc/externals/javelin/lib/URI.js' => 'c989ade3',
256256
'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8',
257257
'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6',
258-
'rsrc/externals/javelin/lib/Workflow.js' => '1e911d0f',
258+
'rsrc/externals/javelin/lib/Workflow.js' => '0eb1db0c',
259259
'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8',
260260
'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b',
261261
'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68',
@@ -757,7 +757,7 @@
757757
'javelin-workboard-card' => 'c587b80f',
758758
'javelin-workboard-column' => '758b4758',
759759
'javelin-workboard-controller' => '26167537',
760-
'javelin-workflow' => '1e911d0f',
760+
'javelin-workflow' => '0eb1db0c',
761761
'maniphest-report-css' => '9b9580b7',
762762
'maniphest-task-edit-css' => 'fda62a9b',
763763
'maniphest-task-summary-css' => '11cc5344',
@@ -977,6 +977,17 @@
977977
'javelin-dom',
978978
'javelin-router',
979979
),
980+
'0eb1db0c' => array(
981+
'javelin-stratcom',
982+
'javelin-request',
983+
'javelin-dom',
984+
'javelin-vector',
985+
'javelin-install',
986+
'javelin-util',
987+
'javelin-mask',
988+
'javelin-uri',
989+
'javelin-routable',
990+
),
980991
'0f764c35' => array(
981992
'javelin-install',
982993
'javelin-util',
@@ -1035,17 +1046,6 @@
10351046
'javelin-request',
10361047
'javelin-uri',
10371048
),
1038-
'1e911d0f' => array(
1039-
'javelin-stratcom',
1040-
'javelin-request',
1041-
'javelin-dom',
1042-
'javelin-vector',
1043-
'javelin-install',
1044-
'javelin-util',
1045-
'javelin-mask',
1046-
'javelin-uri',
1047-
'javelin-routable',
1048-
),
10491049
'1f6794f6' => array(
10501050
'javelin-behavior',
10511051
'javelin-stratcom',

‎src/aphront/response/AphrontRedirectResponse.php

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class AphrontRedirectResponse extends AphrontResponse {
88
private $uri;
99
private $stackWhenCreated;
1010
private $isExternal;
11+
private $closeDialogBeforeRedirect;
1112

1213
public function setIsExternal($external) {
1314
$this->isExternal = $external;
@@ -37,6 +38,15 @@ public function shouldStopForDebugging() {
3738
return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect');
3839
}
3940

41+
public function setCloseDialogBeforeRedirect($close) {
42+
$this->closeDialogBeforeRedirect = $close;
43+
return $this;
44+
}
45+
46+
public function getCloseDialogBeforeRedirect() {
47+
return $this->closeDialogBeforeRedirect;
48+
}
49+
4050
public function getHeaders() {
4151
$headers = array();
4252
if (!$this->shouldStopForDebugging()) {

‎src/aphront/response/AphrontResponse.php

+7
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ private function newContentSecurityPolicyHeader() {
147147
// Block relics of the old world: Flash, Java applets, and so on.
148148
$csp[] = "object-src 'none'";
149149

150+
// Don't allow forms to submit offsite.
151+
152+
// This can result in some trickiness with file downloads if applications
153+
// try to start downloads by submitting a dialog. Redirect to the file's
154+
// download URI instead of submitting a form to it.
155+
$csp[] = "form-action 'self'";
156+
150157
$csp = implode('; ', $csp);
151158

152159
return $csp;

‎src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php

+20-10
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ public function handleRequest(AphrontRequest $request) {
2424
$keys = PhabricatorSSHKeyGenerator::generateKeypair();
2525
list($public_key, $private_key) = $keys;
2626

27+
$key_name = $default_name.'.key';
28+
2729
$file = PhabricatorFile::newFromFileData(
2830
$private_key,
2931
array(
30-
'name' => $default_name.'.key',
32+
'name' => $key_name,
3133
'ttl.relative' => phutil_units('10 minutes in seconds'),
3234
'viewPolicy' => $viewer->getPHID(),
3335
));
@@ -62,25 +64,33 @@ public function handleRequest(AphrontRequest $request) {
6264
->setContentSourceFromRequest($request)
6365
->applyTransactions($key, $xactions);
6466

65-
// NOTE: We're disabling workflow on submit so the download works. We're
66-
// disabling workflow on cancel so the page reloads, showing the new
67-
// key.
67+
$download_link = phutil_tag(
68+
'a',
69+
array(
70+
'href' => $file->getDownloadURI(),
71+
),
72+
array(
73+
id(new PHUIIconView())->setIcon('fa-download'),
74+
' ',
75+
pht('Download Private Key (%s)', $key_name),
76+
));
77+
$download_link = phutil_tag('strong', array(), $download_link);
78+
79+
// NOTE: We're disabling workflow on cancel so the page reloads, showing
80+
// the new key.
6881

6982
return $this->newDialog()
7083
->setTitle(pht('Download Private Key'))
71-
->setDisableWorkflowOnCancel(true)
72-
->setDisableWorkflowOnSubmit(true)
73-
->setSubmitURI($file->getDownloadURI())
7484
->appendParagraph(
7585
pht(
7686
'A keypair has been generated, and the public key has been '.
77-
'added as a recognized key. Use the button below to download '.
78-
'the private key.'))
87+
'added as a recognized key.'))
88+
->appendParagraph($download_link)
7989
->appendParagraph(
8090
pht(
8191
'After you download the private key, it will be destroyed. '.
8292
'You will not be able to retrieve it if you lose your copy.'))
83-
->addSubmitButton(pht('Download Private Key'))
93+
->setDisableWorkflowOnCancel(true)
8494
->addCancelButton($cancel_uri, pht('Done'));
8595
}
8696

‎src/applications/base/controller/PhabricatorController.php

+1
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ public function willSendResponse(AphrontResponse $response) {
298298
->setContent(
299299
array(
300300
'redirect' => $response->getURI(),
301+
'close' => $response->getCloseDialogBeforeRedirect(),
301302
));
302303
}
303304
}

‎src/applications/diffusion/controller/DiffusionServeController.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -1046,7 +1046,7 @@ private function serveGitLFSBatchRequest(
10461046
// <https://github.com/github/git-lfs/issues/1088>
10471047
$no_authorization = 'Basic '.base64_encode('none');
10481048

1049-
$get_uri = $file->getCDNURI();
1049+
$get_uri = $file->getCDNURI('data');
10501050
$actions['download'] = array(
10511051
'href' => $get_uri,
10521052
'header' => array(

‎src/applications/files/application/PhabricatorFilesApplication.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ public function getResourceRoutes() {
101101

102102
private function getResourceSubroutes() {
103103
return array(
104-
'data/'.
104+
'(?P<kind>data|download)/'.
105105
'(?:@(?P<instance>[^/]+)/)?'.
106106
'(?P<key>[^/]+)/'.
107107
'(?P<phid>[^/]+)/'.
@@ -132,7 +132,7 @@ public function getMailCommandObjects() {
132132

133133
public function getQuicksandURIPatternBlacklist() {
134134
return array(
135-
'/file/data/.*',
135+
'/file/(data|download)/.*',
136136
);
137137
}
138138

‎src/applications/files/controller/PhabricatorFileDataController.php

+19-20
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public function handleRequest(AphrontRequest $request) {
2626
$req_domain = $request->getHost();
2727
$main_domain = id(new PhutilURI($base_uri))->getDomain();
2828

29+
$request_kind = $request->getURIData('kind');
30+
$is_download = ($request_kind === 'download');
31+
2932
if (!strlen($alt) || $main_domain == $alt_domain) {
3033
// No alternate domain.
3134
$should_redirect = false;
@@ -50,7 +53,7 @@ public function handleRequest(AphrontRequest $request) {
5053
if ($should_redirect) {
5154
return id(new AphrontRedirectResponse())
5255
->setIsExternal(true)
53-
->setURI($file->getCDNURI());
56+
->setURI($file->getCDNURI($request_kind));
5457
}
5558

5659
$response = new AphrontFileResponse();
@@ -71,34 +74,30 @@ public function handleRequest(AphrontRequest $request) {
7174
}
7275

7376
$is_viewable = $file->isViewableInBrowser();
74-
$force_download = $request->getExists('download');
75-
7677
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
7778
$is_lfs = ($request_type == 'git-lfs');
7879

79-
if ($is_viewable && !$force_download) {
80+
if ($is_viewable && !$is_download) {
8081
$response->setMimeType($file->getViewableMimeType());
8182
} else {
82-
$is_public = !$viewer->isLoggedIn();
8383
$is_post = $request->isHTTPPost();
8484

85-
// NOTE: Require POST to download files from the primary domain if the
86-
// request includes credentials. The "Download File" links we generate
87-
// in the web UI are forms which use POST to satisfy this requirement.
88-
89-
// The intent is to make attacks based on tags like "<iframe />" and
90-
// "<script />" (which can issue GET requests, but can not easily issue
91-
// POST requests) more difficult to execute.
92-
93-
// The best defense against these attacks is to use an alternate file
94-
// domain, which is why we strongly recommend doing so.
85+
// NOTE: Require POST to download files from the primary domain. If the
86+
// request is not a POST request but arrives on the primary domain, we
87+
// render a confirmation dialog. For discussion, see T13094.
9588

96-
$is_safe = ($is_alternate_domain || $is_lfs || $is_post || $is_public);
89+
$is_safe = ($is_alternate_domain || $is_lfs || $is_post);
9790
if (!$is_safe) {
98-
// This is marked as "external" because it is fully qualified.
99-
return id(new AphrontRedirectResponse())
100-
->setIsExternal(true)
101-
->setURI(PhabricatorEnv::getProductionURI($file->getBestURI()));
91+
return $this->newDialog()
92+
->setSubmitURI($file->getDownloadURI())
93+
->setTitle(pht('Download File'))
94+
->appendParagraph(
95+
pht(
96+
'Download file %s (%s)?',
97+
phutil_tag('strong', array(), $file->getName()),
98+
phutil_format_bytes($file->getByteSize())))
99+
->addCancelButton($file->getURI())
100+
->addSubmitButton(pht('Download File'));
102101
}
103102

104103
$response->setMimeType($file->getMimeType());

‎src/applications/files/controller/PhabricatorFileInfoController.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,10 @@ private function buildCurtainView(PhabricatorFile $file) {
137137
$curtain->addAction(
138138
id(new PhabricatorActionView())
139139
->setUser($viewer)
140-
->setRenderAsForm($can_download)
141140
->setDownload($can_download)
142141
->setName(pht('Download File'))
143142
->setIcon('fa-download')
144-
->setHref($file->getViewURI())
143+
->setHref($file->getDownloadURI())
145144
->setDisabled(!$can_download)
146145
->setWorkflow(!$can_download));
147146
}

‎src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ private function renderImageFile(
144144

145145
$existing_xform = $file->getTransform($preview_key);
146146
if ($existing_xform) {
147-
$xform_uri = $existing_xform->getCDNURI();
147+
$xform_uri = $existing_xform->getCDNURI('data');
148148
} else {
149149
$xform_uri = $file->getURIForTransform($xform);
150150
}

‎src/applications/files/storage/PhabricatorFile.php

+22-6
Original file line numberDiff line numberDiff line change
@@ -810,16 +810,24 @@ public function getViewURI() {
810810
pht('You must save a file before you can generate a view URI.'));
811811
}
812812

813-
return $this->getCDNURI();
813+
return $this->getCDNURI('data');
814814
}
815815

816-
public function getCDNURI() {
816+
public function getCDNURI($request_kind) {
817+
if (($request_kind !== 'data') &&
818+
($request_kind !== 'download')) {
819+
throw new Exception(
820+
pht(
821+
'Unknown file content request kind "%s".',
822+
$request_kind));
823+
}
824+
817825
$name = self::normalizeFileName($this->getName());
818826
$name = phutil_escape_uri($name);
819827

820828
$parts = array();
821829
$parts[] = 'file';
822-
$parts[] = 'data';
830+
$parts[] = $request_kind;
823831

824832
// If this is an instanced install, add the instance identifier to the URI.
825833
// Instanced configurations behind a CDN may not be able to control the
@@ -861,9 +869,7 @@ public function getBestURI() {
861869
}
862870

863871
public function getDownloadURI() {
864-
$uri = id(new PhutilURI($this->getViewURI()))
865-
->setQueryParam('download', true);
866-
return (string)$uri;
872+
return $this->getCDNURI('download');
867873
}
868874

869875
public function getURIForTransform(PhabricatorFileTransform $transform) {
@@ -1469,6 +1475,16 @@ public function getRedirectResponse() {
14691475
->setURI($uri);
14701476
}
14711477

1478+
public function newDownloadResponse() {
1479+
// We're cheating a little bit here and relying on the fact that
1480+
// getDownloadURI() always returns a fully qualified URI with a complete
1481+
// domain.
1482+
return id(new AphrontRedirectResponse())
1483+
->setIsExternal(true)
1484+
->setCloseDialogBeforeRedirect(true)
1485+
->setURI($this->getDownloadURI());
1486+
}
1487+
14721488
public function attachTransforms(array $map) {
14731489
$this->transforms = $map;
14741490
return $this;

‎src/applications/harbormaster/controller/HarbormasterBuildLogDownloadController.php

+1-13
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,7 @@ public function handleRequest(AphrontRequest $request) {
4444
->addCancelButton($cancel_uri);
4545
}
4646

47-
$size = $file->getByteSize();
48-
49-
return $this->newDialog()
50-
->setTitle(pht('Download Build Log'))
51-
->appendParagraph(
52-
pht(
53-
'This log has a total size of %s. If you insist, you may '.
54-
'download it.',
55-
phutil_tag('strong', array(), phutil_format_bytes($size))))
56-
->setDisableWorkflowOnSubmit(true)
57-
->addSubmitButton(pht('Download Log'))
58-
->setSubmitURI($file->getDownloadURI())
59-
->addCancelButton($cancel_uri, pht('Done'));
47+
return $file->newDownloadResponse();
6048
}
6149

6250
}

‎src/applications/harbormaster/view/HarbormasterBuildLogView.php

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ public function render() {
3434

3535
$download_uri = "/harbormaster/log/download/{$id}/";
3636

37+
$can_download = (bool)$log->getFilePHID();
38+
3739
$download_button = id(new PHUIButtonView())
3840
->setTag('a')
3941
->setHref($download_uri)
4042
->setIcon('fa-download')
41-
->setDisabled(!$log->getFilePHID())
42-
->setWorkflow(true)
43+
->setDisabled(!$can_download)
44+
->setWorkflow(!$can_download)
4345
->setText(pht('Download Log'));
4446

4547
$header->addActionLink($download_button);

0 commit comments

Comments
 (0)
Failed to load comments.