1<?php
2/**
3 * @copyright Copyright (c) 2016, ownCloud, Inc.
4 *
5 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
6 * @author Lukas Reschke <lukas@statuscode.ch>
7 * @author Pavel Krasikov <klonishe@gmail.com>
8 * @author Pierre Rudloff <contact@rudloff.pro>
9 * @author Roeland Jago Douma <roeland@famdouma.nl>
10 * @author Thomas Citharel <nextcloud@tcit.fr>
11 *
12 * @license AGPL-3.0
13 *
14 * This code is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU Affero General Public License, version 3,
16 * as published by the Free Software Foundation.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU Affero General Public License for more details.
22 *
23 * You should have received a copy of the GNU Affero General Public License, version 3,
24 * along with this program. If not, see <http://www.gnu.org/licenses/>
25 *
26 */
27namespace OCP\AppFramework\Http;
28
29/**
30 * Class EmptyContentSecurityPolicy is a simple helper which allows applications
31 * to modify the Content-Security-Policy sent by Nexcloud. Per default the policy
32 * is forbidding everything.
33 *
34 * As alternative with sane exemptions look at ContentSecurityPolicy
35 *
36 * @see \OCP\AppFramework\Http\ContentSecurityPolicy
37 * @since 9.0.0
38 */
39class EmptyContentSecurityPolicy {
40	/** @var bool Whether inline JS snippets are allowed */
41	protected $inlineScriptAllowed = null;
42	/** @var string Whether JS nonces should be used */
43	protected $useJsNonce = null;
44	/**
45	 * @var bool Whether eval in JS scripts is allowed
46	 * TODO: Disallow per default
47	 * @link https://github.com/owncloud/core/issues/11925
48	 */
49	protected $evalScriptAllowed = null;
50	/** @var array Domains from which scripts can get loaded */
51	protected $allowedScriptDomains = null;
52	/**
53	 * @var bool Whether inline CSS is allowed
54	 * TODO: Disallow per default
55	 * @link https://github.com/owncloud/core/issues/13458
56	 */
57	protected $inlineStyleAllowed = null;
58	/** @var array Domains from which CSS can get loaded */
59	protected $allowedStyleDomains = null;
60	/** @var array Domains from which images can get loaded */
61	protected $allowedImageDomains = null;
62	/** @var array Domains to which connections can be done */
63	protected $allowedConnectDomains = null;
64	/** @var array Domains from which media elements can be loaded */
65	protected $allowedMediaDomains = null;
66	/** @var array Domains from which object elements can be loaded */
67	protected $allowedObjectDomains = null;
68	/** @var array Domains from which iframes can be loaded */
69	protected $allowedFrameDomains = null;
70	/** @var array Domains from which fonts can be loaded */
71	protected $allowedFontDomains = null;
72	/** @var array Domains from which web-workers and nested browsing content can load elements */
73	protected $allowedChildSrcDomains = null;
74	/** @var array Domains which can embed this Nextcloud instance */
75	protected $allowedFrameAncestors = null;
76	/** @var array Domains from which web-workers can be loaded */
77	protected $allowedWorkerSrcDomains = null;
78	/** @var array Domains which can be used as target for forms */
79	protected $allowedFormActionDomains = null;
80
81	/** @var array Locations to report violations to */
82	protected $reportTo = null;
83
84	/**
85	 * Whether inline JavaScript snippets are allowed or forbidden
86	 * @param bool $state
87	 * @return $this
88	 * @since 8.1.0
89	 * @deprecated 10.0 CSP tokens are now used
90	 */
91	public function allowInlineScript($state = false) {
92		$this->inlineScriptAllowed = $state;
93		return $this;
94	}
95
96	/**
97	 * Use the according JS nonce
98	 * This method is only for CSPMiddleware, custom values are ignored in mergePolicies of ContentSecurityPolicyManager
99	 *
100	 * @param string $nonce
101	 * @return $this
102	 * @since 11.0.0
103	 */
104	public function useJsNonce($nonce) {
105		$this->useJsNonce = $nonce;
106		return $this;
107	}
108
109	/**
110	 * Whether eval in JavaScript is allowed or forbidden
111	 * @param bool $state
112	 * @return $this
113	 * @since 8.1.0
114	 * @deprecated Eval should not be used anymore. Please update your scripts. This function will stop functioning in a future version of Nextcloud.
115	 */
116	public function allowEvalScript($state = true) {
117		$this->evalScriptAllowed = $state;
118		return $this;
119	}
120
121	/**
122	 * Allows to execute JavaScript files from a specific domain. Use * to
123	 * allow JavaScript from all domains.
124	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
125	 * @return $this
126	 * @since 8.1.0
127	 */
128	public function addAllowedScriptDomain($domain) {
129		$this->allowedScriptDomains[] = $domain;
130		return $this;
131	}
132
133	/**
134	 * Remove the specified allowed script domain from the allowed domains.
135	 *
136	 * @param string $domain
137	 * @return $this
138	 * @since 8.1.0
139	 */
140	public function disallowScriptDomain($domain) {
141		$this->allowedScriptDomains = array_diff($this->allowedScriptDomains, [$domain]);
142		return $this;
143	}
144
145	/**
146	 * Whether inline CSS snippets are allowed or forbidden
147	 * @param bool $state
148	 * @return $this
149	 * @since 8.1.0
150	 */
151	public function allowInlineStyle($state = true) {
152		$this->inlineStyleAllowed = $state;
153		return $this;
154	}
155
156	/**
157	 * Allows to execute CSS files from a specific domain. Use * to allow
158	 * CSS from all domains.
159	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
160	 * @return $this
161	 * @since 8.1.0
162	 */
163	public function addAllowedStyleDomain($domain) {
164		$this->allowedStyleDomains[] = $domain;
165		return $this;
166	}
167
168	/**
169	 * Remove the specified allowed style domain from the allowed domains.
170	 *
171	 * @param string $domain
172	 * @return $this
173	 * @since 8.1.0
174	 */
175	public function disallowStyleDomain($domain) {
176		$this->allowedStyleDomains = array_diff($this->allowedStyleDomains, [$domain]);
177		return $this;
178	}
179
180	/**
181	 * Allows using fonts from a specific domain. Use * to allow
182	 * fonts from all domains.
183	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
184	 * @return $this
185	 * @since 8.1.0
186	 */
187	public function addAllowedFontDomain($domain) {
188		$this->allowedFontDomains[] = $domain;
189		return $this;
190	}
191
192	/**
193	 * Remove the specified allowed font domain from the allowed domains.
194	 *
195	 * @param string $domain
196	 * @return $this
197	 * @since 8.1.0
198	 */
199	public function disallowFontDomain($domain) {
200		$this->allowedFontDomains = array_diff($this->allowedFontDomains, [$domain]);
201		return $this;
202	}
203
204	/**
205	 * Allows embedding images from a specific domain. Use * to allow
206	 * images from all domains.
207	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
208	 * @return $this
209	 * @since 8.1.0
210	 */
211	public function addAllowedImageDomain($domain) {
212		$this->allowedImageDomains[] = $domain;
213		return $this;
214	}
215
216	/**
217	 * Remove the specified allowed image domain from the allowed domains.
218	 *
219	 * @param string $domain
220	 * @return $this
221	 * @since 8.1.0
222	 */
223	public function disallowImageDomain($domain) {
224		$this->allowedImageDomains = array_diff($this->allowedImageDomains, [$domain]);
225		return $this;
226	}
227
228	/**
229	 * To which remote domains the JS connect to.
230	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
231	 * @return $this
232	 * @since 8.1.0
233	 */
234	public function addAllowedConnectDomain($domain) {
235		$this->allowedConnectDomains[] = $domain;
236		return $this;
237	}
238
239	/**
240	 * Remove the specified allowed connect domain from the allowed domains.
241	 *
242	 * @param string $domain
243	 * @return $this
244	 * @since 8.1.0
245	 */
246	public function disallowConnectDomain($domain) {
247		$this->allowedConnectDomains = array_diff($this->allowedConnectDomains, [$domain]);
248		return $this;
249	}
250
251	/**
252	 * From which domains media elements can be embedded.
253	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
254	 * @return $this
255	 * @since 8.1.0
256	 */
257	public function addAllowedMediaDomain($domain) {
258		$this->allowedMediaDomains[] = $domain;
259		return $this;
260	}
261
262	/**
263	 * Remove the specified allowed media domain from the allowed domains.
264	 *
265	 * @param string $domain
266	 * @return $this
267	 * @since 8.1.0
268	 */
269	public function disallowMediaDomain($domain) {
270		$this->allowedMediaDomains = array_diff($this->allowedMediaDomains, [$domain]);
271		return $this;
272	}
273
274	/**
275	 * From which domains objects such as <object>, <embed> or <applet> are executed
276	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
277	 * @return $this
278	 * @since 8.1.0
279	 */
280	public function addAllowedObjectDomain($domain) {
281		$this->allowedObjectDomains[] = $domain;
282		return $this;
283	}
284
285	/**
286	 * Remove the specified allowed object domain from the allowed domains.
287	 *
288	 * @param string $domain
289	 * @return $this
290	 * @since 8.1.0
291	 */
292	public function disallowObjectDomain($domain) {
293		$this->allowedObjectDomains = array_diff($this->allowedObjectDomains, [$domain]);
294		return $this;
295	}
296
297	/**
298	 * Which domains can be embedded in an iframe
299	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
300	 * @return $this
301	 * @since 8.1.0
302	 */
303	public function addAllowedFrameDomain($domain) {
304		$this->allowedFrameDomains[] = $domain;
305		return $this;
306	}
307
308	/**
309	 * Remove the specified allowed frame domain from the allowed domains.
310	 *
311	 * @param string $domain
312	 * @return $this
313	 * @since 8.1.0
314	 */
315	public function disallowFrameDomain($domain) {
316		$this->allowedFrameDomains = array_diff($this->allowedFrameDomains, [$domain]);
317		return $this;
318	}
319
320	/**
321	 * Domains from which web-workers and nested browsing content can load elements
322	 * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
323	 * @return $this
324	 * @since 8.1.0
325	 * @deprecated 15.0.0 use addAllowedWorkerSrcDomains or addAllowedFrameDomain
326	 */
327	public function addAllowedChildSrcDomain($domain) {
328		$this->allowedChildSrcDomains[] = $domain;
329		return $this;
330	}
331
332	/**
333	 * Remove the specified allowed child src domain from the allowed domains.
334	 *
335	 * @param string $domain
336	 * @return $this
337	 * @since 8.1.0
338	 * @deprecated 15.0.0 use the WorkerSrcDomains or FrameDomain
339	 */
340	public function disallowChildSrcDomain($domain) {
341		$this->allowedChildSrcDomains = array_diff($this->allowedChildSrcDomains, [$domain]);
342		return $this;
343	}
344
345	/**
346	 * Domains which can embed an iFrame of the Nextcloud instance
347	 *
348	 * @param string $domain
349	 * @return $this
350	 * @since 13.0.0
351	 */
352	public function addAllowedFrameAncestorDomain($domain) {
353		$this->allowedFrameAncestors[] = $domain;
354		return $this;
355	}
356
357	/**
358	 * Domains which can embed an iFrame of the Nextcloud instance
359	 *
360	 * @param string $domain
361	 * @return $this
362	 * @since 13.0.0
363	 */
364	public function disallowFrameAncestorDomain($domain) {
365		$this->allowedFrameAncestors = array_diff($this->allowedFrameAncestors, [$domain]);
366		return $this;
367	}
368
369	/**
370	 * Domain from which workers can be loaded
371	 *
372	 * @param string $domain
373	 * @return $this
374	 * @since 15.0.0
375	 */
376	public function addAllowedWorkerSrcDomain(string $domain) {
377		$this->allowedWorkerSrcDomains[] = $domain;
378		return $this;
379	}
380
381	/**
382	 * Remove domain from which workers can be loaded
383	 *
384	 * @param string $domain
385	 * @return $this
386	 * @since 15.0.0
387	 */
388	public function disallowWorkerSrcDomain(string $domain) {
389		$this->allowedWorkerSrcDomains = array_diff($this->allowedWorkerSrcDomains, [$domain]);
390		return $this;
391	}
392
393	/**
394	 * Domain to where forms can submit
395	 *
396	 * @since 17.0.0
397	 *
398	 * @return $this
399	 */
400	public function addAllowedFormActionDomain(string $domain) {
401		$this->allowedFormActionDomains[] = $domain;
402		return $this;
403	}
404
405	/**
406	 * Remove domain to where forms can submit
407	 *
408	 * @return $this
409	 * @since 17.0.0
410	 */
411	public function disallowFormActionDomain(string $domain) {
412		$this->allowedFormActionDomains = array_diff($this->allowedFormActionDomains, [$domain]);
413		return $this;
414	}
415
416	/**
417	 * Add location to report CSP violations to
418	 *
419	 * @param string $location
420	 * @return $this
421	 * @since 15.0.0
422	 */
423	public function addReportTo(string $location) {
424		$this->reportTo[] = $location;
425		return $this;
426	}
427
428	/**
429	 * Get the generated Content-Security-Policy as a string
430	 * @return string
431	 * @since 8.1.0
432	 */
433	public function buildPolicy() {
434		$policy = "default-src 'none';";
435		$policy .= "base-uri 'none';";
436		$policy .= "manifest-src 'self';";
437
438		if (!empty($this->allowedScriptDomains) || $this->inlineScriptAllowed || $this->evalScriptAllowed) {
439			$policy .= 'script-src ';
440			if (is_string($this->useJsNonce)) {
441				$policy .= '\'nonce-'.base64_encode($this->useJsNonce).'\'';
442				$allowedScriptDomains = array_flip($this->allowedScriptDomains);
443				unset($allowedScriptDomains['\'self\'']);
444				$this->allowedScriptDomains = array_flip($allowedScriptDomains);
445				if (count($allowedScriptDomains) !== 0) {
446					$policy .= ' ';
447				}
448			}
449			if (is_array($this->allowedScriptDomains)) {
450				$policy .= implode(' ', $this->allowedScriptDomains);
451			}
452			if ($this->inlineScriptAllowed) {
453				$policy .= ' \'unsafe-inline\'';
454			}
455			if ($this->evalScriptAllowed) {
456				$policy .= ' \'unsafe-eval\'';
457			}
458			$policy .= ';';
459		}
460
461		if (!empty($this->allowedStyleDomains) || $this->inlineStyleAllowed) {
462			$policy .= 'style-src ';
463			if (is_array($this->allowedStyleDomains)) {
464				$policy .= implode(' ', $this->allowedStyleDomains);
465			}
466			if ($this->inlineStyleAllowed) {
467				$policy .= ' \'unsafe-inline\'';
468			}
469			$policy .= ';';
470		}
471
472		if (!empty($this->allowedImageDomains)) {
473			$policy .= 'img-src ' . implode(' ', $this->allowedImageDomains);
474			$policy .= ';';
475		}
476
477		if (!empty($this->allowedFontDomains)) {
478			$policy .= 'font-src ' . implode(' ', $this->allowedFontDomains);
479			$policy .= ';';
480		}
481
482		if (!empty($this->allowedConnectDomains)) {
483			$policy .= 'connect-src ' . implode(' ', $this->allowedConnectDomains);
484			$policy .= ';';
485		}
486
487		if (!empty($this->allowedMediaDomains)) {
488			$policy .= 'media-src ' . implode(' ', $this->allowedMediaDomains);
489			$policy .= ';';
490		}
491
492		if (!empty($this->allowedObjectDomains)) {
493			$policy .= 'object-src ' . implode(' ', $this->allowedObjectDomains);
494			$policy .= ';';
495		}
496
497		if (!empty($this->allowedFrameDomains)) {
498			$policy .= 'frame-src ';
499			$policy .= implode(' ', $this->allowedFrameDomains);
500			$policy .= ';';
501		}
502
503		if (!empty($this->allowedChildSrcDomains)) {
504			$policy .= 'child-src ' . implode(' ', $this->allowedChildSrcDomains);
505			$policy .= ';';
506		}
507
508		if (!empty($this->allowedFrameAncestors)) {
509			$policy .= 'frame-ancestors ' . implode(' ', $this->allowedFrameAncestors);
510			$policy .= ';';
511		} else {
512			$policy .= 'frame-ancestors \'none\';';
513		}
514
515		if (!empty($this->allowedWorkerSrcDomains)) {
516			$policy .= 'worker-src ' . implode(' ', $this->allowedWorkerSrcDomains);
517			$policy .= ';';
518		}
519
520		if (!empty($this->allowedFormActionDomains)) {
521			$policy .= 'form-action ' . implode(' ', $this->allowedFormActionDomains);
522			$policy .= ';';
523		}
524
525		if (!empty($this->reportTo)) {
526			$policy .= 'report-uri ' . implode(' ', $this->reportTo);
527			$policy .= ';';
528		}
529
530		return rtrim($policy, ';');
531	}
532}
533