Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/aphront/response/AphrontResponse.php
12241 views
1
<?php
2
3
abstract class AphrontResponse extends Phobject {
4
5
private $request;
6
private $cacheable = false;
7
private $canCDN;
8
private $responseCode = 200;
9
private $lastModified = null;
10
private $contentSecurityPolicyURIs;
11
private $disableContentSecurityPolicy;
12
protected $frameable;
13
private $headers = array();
14
15
public function setRequest($request) {
16
$this->request = $request;
17
return $this;
18
}
19
20
public function getRequest() {
21
return $this->request;
22
}
23
24
final public function addContentSecurityPolicyURI($kind, $uri) {
25
if ($this->contentSecurityPolicyURIs === null) {
26
$this->contentSecurityPolicyURIs = array(
27
'script-src' => array(),
28
'connect-src' => array(),
29
'frame-src' => array(),
30
'form-action' => array(),
31
'object-src' => array(),
32
);
33
}
34
35
if (!isset($this->contentSecurityPolicyURIs[$kind])) {
36
throw new Exception(
37
pht(
38
'Unknown Content-Security-Policy URI kind "%s".',
39
$kind));
40
}
41
42
$this->contentSecurityPolicyURIs[$kind][] = (string)$uri;
43
44
return $this;
45
}
46
47
final public function setDisableContentSecurityPolicy($disable) {
48
$this->disableContentSecurityPolicy = $disable;
49
return $this;
50
}
51
52
final public function addHeader($key, $value) {
53
$this->headers[] = array($key, $value);
54
return $this;
55
}
56
57
58
/* -( Content )------------------------------------------------------------ */
59
60
61
public function getContentIterator() {
62
// By default, make sure responses are truly returning a string, not some
63
// kind of object that behaves like a string.
64
65
// We're going to remove the execution time limit before dumping the
66
// response into the sink, and want any rendering that's going to occur
67
// to happen BEFORE we release the limit.
68
69
return array(
70
(string)$this->buildResponseString(),
71
);
72
}
73
74
public function buildResponseString() {
75
throw new PhutilMethodNotImplementedException();
76
}
77
78
79
/* -( Metadata )----------------------------------------------------------- */
80
81
82
public function getHeaders() {
83
$headers = array();
84
if (!$this->frameable) {
85
$headers[] = array('X-Frame-Options', 'Deny');
86
}
87
88
if ($this->getRequest() && $this->getRequest()->isHTTPS()) {
89
$hsts_key = 'security.strict-transport-security';
90
$use_hsts = PhabricatorEnv::getEnvConfig($hsts_key);
91
if ($use_hsts) {
92
$duration = phutil_units('365 days in seconds');
93
} else {
94
// If HSTS has been disabled, tell browsers to turn it off. This may
95
// not be effective because we can only disable it over a valid HTTPS
96
// connection, but it best represents the configured intent.
97
$duration = 0;
98
}
99
100
$headers[] = array(
101
'Strict-Transport-Security',
102
"max-age={$duration}; includeSubdomains; preload",
103
);
104
}
105
106
$csp = $this->newContentSecurityPolicyHeader();
107
if ($csp !== null) {
108
$headers[] = array('Content-Security-Policy', $csp);
109
}
110
111
$headers[] = array('Referrer-Policy', 'no-referrer');
112
113
foreach ($this->headers as $header) {
114
$headers[] = $header;
115
}
116
117
return $headers;
118
}
119
120
private function newContentSecurityPolicyHeader() {
121
if ($this->disableContentSecurityPolicy) {
122
return null;
123
}
124
125
// NOTE: We may return a response during preflight checks (for example,
126
// if a user has a bad version of PHP).
127
128
// In this case, setup isn't complete yet and we can't access environmental
129
// configuration. If we aren't able to read the environment, just decline
130
// to emit a Content-Security-Policy header.
131
132
try {
133
$cdn = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
134
$base_uri = PhabricatorEnv::getURI('/');
135
} catch (Exception $ex) {
136
return null;
137
}
138
139
$csp = array();
140
if ($cdn) {
141
$default = $this->newContentSecurityPolicySource($cdn);
142
} else {
143
// If an alternate file domain is not configured and the user is viewing
144
// a Phame blog on a custom domain or some other custom site, we'll still
145
// serve resources from the main site. Include the main site explicitly.
146
$base_uri = $this->newContentSecurityPolicySource($base_uri);
147
148
$default = "'self' {$base_uri}";
149
}
150
151
$csp[] = "default-src {$default}";
152
153
// We use "data:" URIs to inline small images into CSS. This policy allows
154
// "data:" URIs to be used anywhere, but there doesn't appear to be a way
155
// to say that "data:" URIs are okay in CSS files but not in the document.
156
$csp[] = "img-src {$default} data:";
157
158
// We use inline style="..." attributes in various places, many of which
159
// are legitimate. We also currently use a <style> tag to implement the
160
// "Monospaced Font Preference" setting.
161
$csp[] = "style-src {$default} 'unsafe-inline'";
162
163
// On a small number of pages, including the Stripe workflow and the
164
// ReCAPTCHA challenge, we embed external Javascript directly.
165
$csp[] = $this->newContentSecurityPolicy('script-src', $default);
166
167
// We need to specify that we can connect to ourself in order for AJAX
168
// requests to work.
169
$csp[] = $this->newContentSecurityPolicy('connect-src', "'self'");
170
171
// DarkConsole and PHPAST both use frames to render some content.
172
$csp[] = $this->newContentSecurityPolicy('frame-src', "'self'");
173
174
// This is a more modern flavor of of "X-Frame-Options" and prevents
175
// clickjacking attacks where the page is included in a tiny iframe and
176
// the user is convinced to click a element on the page, which really
177
// clicks a dangerous button hidden under a picture of a cat.
178
if ($this->frameable) {
179
$csp[] = "frame-ancestors 'self'";
180
} else {
181
$csp[] = "frame-ancestors 'none'";
182
}
183
184
// Block relics of the old world: Flash, Java applets, and so on. Note
185
// that Chrome prevents the user from viewing PDF documents if they are
186
// served with a policy which excludes the domain they are served from.
187
$csp[] = $this->newContentSecurityPolicy('object-src', "'none'");
188
189
// Don't allow forms to submit offsite.
190
191
// This can result in some trickiness with file downloads if applications
192
// try to start downloads by submitting a dialog. Redirect to the file's
193
// download URI instead of submitting a form to it.
194
$csp[] = $this->newContentSecurityPolicy('form-action', "'self'");
195
196
// Block use of "<base>" to change the origin of relative URIs on the page.
197
$csp[] = "base-uri 'none'";
198
199
$csp = implode('; ', $csp);
200
201
return $csp;
202
}
203
204
private function newContentSecurityPolicy($type, $defaults) {
205
if ($defaults === null) {
206
$sources = array();
207
} else {
208
$sources = (array)$defaults;
209
}
210
211
$uris = $this->contentSecurityPolicyURIs;
212
if (isset($uris[$type])) {
213
foreach ($uris[$type] as $uri) {
214
$sources[] = $this->newContentSecurityPolicySource($uri);
215
}
216
}
217
$sources = array_unique($sources);
218
219
return $type.' '.implode(' ', $sources);
220
}
221
222
private function newContentSecurityPolicySource($uri) {
223
// Some CSP URIs are ultimately user controlled (like notification server
224
// URIs and CDN URIs) so attempt to stop an attacker from injecting an
225
// unsafe source (like 'unsafe-eval') into the CSP header.
226
227
$uri = id(new PhutilURI($uri))
228
->setPath(null)
229
->setFragment(null)
230
->removeAllQueryParams();
231
232
$uri = (string)$uri;
233
if (preg_match('/[ ;\']/', $uri)) {
234
throw new Exception(
235
pht(
236
'Attempting to emit a response with an unsafe source ("%s") in the '.
237
'Content-Security-Policy header.',
238
$uri));
239
}
240
241
return $uri;
242
}
243
244
public function setCacheDurationInSeconds($duration) {
245
$this->cacheable = $duration;
246
return $this;
247
}
248
249
public function setCanCDN($can_cdn) {
250
$this->canCDN = $can_cdn;
251
return $this;
252
}
253
254
public function setLastModified($epoch_timestamp) {
255
$this->lastModified = $epoch_timestamp;
256
return $this;
257
}
258
259
public function setHTTPResponseCode($code) {
260
$this->responseCode = $code;
261
return $this;
262
}
263
264
public function getHTTPResponseCode() {
265
return $this->responseCode;
266
}
267
268
public function getHTTPResponseMessage() {
269
switch ($this->getHTTPResponseCode()) {
270
case 100: return 'Continue';
271
case 101: return 'Switching Protocols';
272
case 200: return 'OK';
273
case 201: return 'Created';
274
case 202: return 'Accepted';
275
case 203: return 'Non-Authoritative Information';
276
case 204: return 'No Content';
277
case 205: return 'Reset Content';
278
case 206: return 'Partial Content';
279
case 300: return 'Multiple Choices';
280
case 301: return 'Moved Permanently';
281
case 302: return 'Found';
282
case 303: return 'See Other';
283
case 304: return 'Not Modified';
284
case 305: return 'Use Proxy';
285
case 306: return 'Switch Proxy';
286
case 307: return 'Temporary Redirect';
287
case 400: return 'Bad Request';
288
case 401: return 'Unauthorized';
289
case 402: return 'Payment Required';
290
case 403: return 'Forbidden';
291
case 404: return 'Not Found';
292
case 405: return 'Method Not Allowed';
293
case 406: return 'Not Acceptable';
294
case 407: return 'Proxy Authentication Required';
295
case 408: return 'Request Timeout';
296
case 409: return 'Conflict';
297
case 410: return 'Gone';
298
case 411: return 'Length Required';
299
case 412: return 'Precondition Failed';
300
case 413: return 'Request Entity Too Large';
301
case 414: return 'Request-URI Too Long';
302
case 415: return 'Unsupported Media Type';
303
case 416: return 'Requested Range Not Satisfiable';
304
case 417: return 'Expectation Failed';
305
case 418: return "I'm a teapot";
306
case 426: return 'Upgrade Required';
307
case 500: return 'Internal Server Error';
308
case 501: return 'Not Implemented';
309
case 502: return 'Bad Gateway';
310
case 503: return 'Service Unavailable';
311
case 504: return 'Gateway Timeout';
312
case 505: return 'HTTP Version Not Supported';
313
default: return '';
314
}
315
}
316
317
public function setFrameable($frameable) {
318
$this->frameable = $frameable;
319
return $this;
320
}
321
322
public static function processValueForJSONEncoding(&$value, $key) {
323
if ($value instanceof PhutilSafeHTMLProducerInterface) {
324
// This renders the producer down to PhutilSafeHTML, which will then
325
// be simplified into a string below.
326
$value = hsprintf('%s', $value);
327
}
328
329
if ($value instanceof PhutilSafeHTML) {
330
// TODO: Javelin supports implicity conversion of '__html' objects to
331
// JX.HTML, but only for Ajax responses, not behaviors. Just leave things
332
// as they are for now (where behaviors treat responses as HTML or plain
333
// text at their discretion).
334
$value = $value->getHTMLContent();
335
}
336
}
337
338
public static function encodeJSONForHTTPResponse(array $object) {
339
340
array_walk_recursive(
341
$object,
342
array(__CLASS__, 'processValueForJSONEncoding'));
343
344
$response = phutil_json_encode($object);
345
346
// Prevent content sniffing attacks by encoding "<" and ">", so browsers
347
// won't try to execute the document as HTML even if they ignore
348
// Content-Type and X-Content-Type-Options. See T865.
349
$response = str_replace(
350
array('<', '>'),
351
array('\u003c', '\u003e'),
352
$response);
353
354
return $response;
355
}
356
357
protected function addJSONShield($json_response) {
358
// Add a shield to prevent "JSON Hijacking" attacks where an attacker
359
// requests a JSON response using a normal <script /> tag and then uses
360
// Object.prototype.__defineSetter__() or similar to read response data.
361
// This header causes the browser to loop infinitely instead of handing over
362
// sensitive data.
363
364
$shield = 'for (;;);';
365
366
$response = $shield.$json_response;
367
368
return $response;
369
}
370
371
public function getCacheHeaders() {
372
$headers = array();
373
if ($this->cacheable) {
374
$cache_control = array();
375
$cache_control[] = sprintf('max-age=%d', $this->cacheable);
376
377
if ($this->canCDN) {
378
$cache_control[] = 'public';
379
} else {
380
$cache_control[] = 'private';
381
}
382
383
$headers[] = array(
384
'Cache-Control',
385
implode(', ', $cache_control),
386
);
387
388
$headers[] = array(
389
'Expires',
390
$this->formatEpochTimestampForHTTPHeader(time() + $this->cacheable),
391
);
392
} else {
393
$headers[] = array(
394
'Cache-Control',
395
'no-store',
396
);
397
$headers[] = array(
398
'Expires',
399
'Sat, 01 Jan 2000 00:00:00 GMT',
400
);
401
}
402
403
if ($this->lastModified) {
404
$headers[] = array(
405
'Last-Modified',
406
$this->formatEpochTimestampForHTTPHeader($this->lastModified),
407
);
408
}
409
410
// IE has a feature where it may override an explicit Content-Type
411
// declaration by inferring a content type. This can be a security risk
412
// and we always explicitly transmit the correct Content-Type header, so
413
// prevent IE from using inferred content types. This only offers protection
414
// on recent versions of IE; IE6/7 and Opera currently ignore this header.
415
$headers[] = array('X-Content-Type-Options', 'nosniff');
416
417
return $headers;
418
}
419
420
private function formatEpochTimestampForHTTPHeader($epoch_timestamp) {
421
return gmdate('D, d M Y H:i:s', $epoch_timestamp).' GMT';
422
}
423
424
protected function shouldCompressResponse() {
425
return true;
426
}
427
428
public function willBeginWrite() {
429
// If we've already sent headers, these "ini_set()" calls will warn that
430
// they have no effect. Today, this always happens because we're inside
431
// a unit test, so just skip adjusting the setting.
432
433
if (!headers_sent()) {
434
if ($this->shouldCompressResponse()) {
435
// Enable automatic compression here. Webservers sometimes do this for
436
// us, but we now detect the absence of compression and warn users about
437
// it so try to cover our bases more thoroughly.
438
ini_set('zlib.output_compression', 1);
439
} else {
440
ini_set('zlib.output_compression', 0);
441
}
442
}
443
}
444
445
public function didCompleteWrite($aborted) {
446
return;
447
}
448
449
}
450
451