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