EmptyContentSecurityPolicy.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  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. */
  27. namespace OCP\AppFramework\Http;
  28. /**
  29. * Class EmptyContentSecurityPolicy is a simple helper which allows applications
  30. * to modify the Content-Security-Policy sent by Nexcloud. Per default the policy
  31. * is forbidding everything.
  32. *
  33. * As alternative with sane exemptions look at ContentSecurityPolicy
  34. *
  35. * @see \OCP\AppFramework\Http\ContentSecurityPolicy
  36. * @since 9.0.0
  37. */
  38. class EmptyContentSecurityPolicy {
  39. /** @var string JS nonce to be used */
  40. protected $jsNonce = null;
  41. /** @var bool Whether strict-dynamic should be used */
  42. protected $strictDynamicAllowed = null;
  43. /** @var bool Whether strict-dynamic should be used on script-src-elem */
  44. protected $strictDynamicAllowedOnScripts = null;
  45. /**
  46. * @var bool Whether eval in JS scripts is allowed
  47. * TODO: Disallow per default
  48. * @link https://github.com/owncloud/core/issues/11925
  49. */
  50. protected $evalScriptAllowed = null;
  51. /** @var bool Whether WebAssembly compilation is allowed */
  52. protected ?bool $evalWasmAllowed = null;
  53. /** @var array Domains from which scripts can get loaded */
  54. protected $allowedScriptDomains = null;
  55. /**
  56. * @var bool Whether inline CSS is allowed
  57. * TODO: Disallow per default
  58. * @link https://github.com/owncloud/core/issues/13458
  59. */
  60. protected $inlineStyleAllowed = null;
  61. /** @var array Domains from which CSS can get loaded */
  62. protected $allowedStyleDomains = null;
  63. /** @var array Domains from which images can get loaded */
  64. protected $allowedImageDomains = null;
  65. /** @var array Domains to which connections can be done */
  66. protected $allowedConnectDomains = null;
  67. /** @var array Domains from which media elements can be loaded */
  68. protected $allowedMediaDomains = null;
  69. /** @var array Domains from which object elements can be loaded */
  70. protected $allowedObjectDomains = null;
  71. /** @var array Domains from which iframes can be loaded */
  72. protected $allowedFrameDomains = null;
  73. /** @var array Domains from which fonts can be loaded */
  74. protected $allowedFontDomains = null;
  75. /** @var array Domains from which web-workers and nested browsing content can load elements */
  76. protected $allowedChildSrcDomains = null;
  77. /** @var array Domains which can embed this Nextcloud instance */
  78. protected $allowedFrameAncestors = null;
  79. /** @var array Domains from which web-workers can be loaded */
  80. protected $allowedWorkerSrcDomains = null;
  81. /** @var array Domains which can be used as target for forms */
  82. protected $allowedFormActionDomains = null;
  83. /** @var array Locations to report violations to */
  84. protected $reportTo = null;
  85. /**
  86. * @param bool $state
  87. * @return EmptyContentSecurityPolicy
  88. * @since 24.0.0
  89. */
  90. public function useStrictDynamic(bool $state = false): self {
  91. $this->strictDynamicAllowed = $state;
  92. return $this;
  93. }
  94. /**
  95. * In contrast to `useStrictDynamic` this only sets strict-dynamic on script-src-elem
  96. * Meaning only grants trust to all imports of scripts that were loaded in `<script>` tags, and thus weakens less the CSP.
  97. * @param bool $state
  98. * @return EmptyContentSecurityPolicy
  99. * @since 28.0.0
  100. */
  101. public function useStrictDynamicOnScripts(bool $state = false): self {
  102. $this->strictDynamicAllowedOnScripts = $state;
  103. return $this;
  104. }
  105. /**
  106. * Use the according JS nonce
  107. * This method is only for CSPMiddleware, custom values are ignored in mergePolicies of ContentSecurityPolicyManager
  108. *
  109. * @param string $nonce
  110. * @return $this
  111. * @since 11.0.0
  112. */
  113. public function useJsNonce($nonce) {
  114. $this->jsNonce = $nonce;
  115. return $this;
  116. }
  117. /**
  118. * Whether eval in JavaScript is allowed or forbidden
  119. * @param bool $state
  120. * @return $this
  121. * @since 8.1.0
  122. * @deprecated Eval should not be used anymore. Please update your scripts. This function will stop functioning in a future version of Nextcloud.
  123. */
  124. public function allowEvalScript($state = true) {
  125. $this->evalScriptAllowed = $state;
  126. return $this;
  127. }
  128. /**
  129. * Whether WebAssembly compilation is allowed or forbidden
  130. * @param bool $state
  131. * @return $this
  132. * @since 28.0.0
  133. */
  134. public function allowEvalWasm(bool $state = true) {
  135. $this->evalWasmAllowed = $state;
  136. return $this;
  137. }
  138. /**
  139. * Allows to execute JavaScript files from a specific domain. Use * to
  140. * allow JavaScript from all domains.
  141. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  142. * @return $this
  143. * @since 8.1.0
  144. */
  145. public function addAllowedScriptDomain($domain) {
  146. $this->allowedScriptDomains[] = $domain;
  147. return $this;
  148. }
  149. /**
  150. * Remove the specified allowed script domain from the allowed domains.
  151. *
  152. * @param string $domain
  153. * @return $this
  154. * @since 8.1.0
  155. */
  156. public function disallowScriptDomain($domain) {
  157. $this->allowedScriptDomains = array_diff($this->allowedScriptDomains, [$domain]);
  158. return $this;
  159. }
  160. /**
  161. * Whether inline CSS snippets are allowed or forbidden
  162. * @param bool $state
  163. * @return $this
  164. * @since 8.1.0
  165. */
  166. public function allowInlineStyle($state = true) {
  167. $this->inlineStyleAllowed = $state;
  168. return $this;
  169. }
  170. /**
  171. * Allows to execute CSS files from a specific domain. Use * to allow
  172. * CSS from all domains.
  173. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  174. * @return $this
  175. * @since 8.1.0
  176. */
  177. public function addAllowedStyleDomain($domain) {
  178. $this->allowedStyleDomains[] = $domain;
  179. return $this;
  180. }
  181. /**
  182. * Remove the specified allowed style domain from the allowed domains.
  183. *
  184. * @param string $domain
  185. * @return $this
  186. * @since 8.1.0
  187. */
  188. public function disallowStyleDomain($domain) {
  189. $this->allowedStyleDomains = array_diff($this->allowedStyleDomains, [$domain]);
  190. return $this;
  191. }
  192. /**
  193. * Allows using fonts from a specific domain. Use * to allow
  194. * fonts from all domains.
  195. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  196. * @return $this
  197. * @since 8.1.0
  198. */
  199. public function addAllowedFontDomain($domain) {
  200. $this->allowedFontDomains[] = $domain;
  201. return $this;
  202. }
  203. /**
  204. * Remove the specified allowed font domain from the allowed domains.
  205. *
  206. * @param string $domain
  207. * @return $this
  208. * @since 8.1.0
  209. */
  210. public function disallowFontDomain($domain) {
  211. $this->allowedFontDomains = array_diff($this->allowedFontDomains, [$domain]);
  212. return $this;
  213. }
  214. /**
  215. * Allows embedding images from a specific domain. Use * to allow
  216. * images from all domains.
  217. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  218. * @return $this
  219. * @since 8.1.0
  220. */
  221. public function addAllowedImageDomain($domain) {
  222. $this->allowedImageDomains[] = $domain;
  223. return $this;
  224. }
  225. /**
  226. * Remove the specified allowed image domain from the allowed domains.
  227. *
  228. * @param string $domain
  229. * @return $this
  230. * @since 8.1.0
  231. */
  232. public function disallowImageDomain($domain) {
  233. $this->allowedImageDomains = array_diff($this->allowedImageDomains, [$domain]);
  234. return $this;
  235. }
  236. /**
  237. * To which remote domains the JS connect to.
  238. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  239. * @return $this
  240. * @since 8.1.0
  241. */
  242. public function addAllowedConnectDomain($domain) {
  243. $this->allowedConnectDomains[] = $domain;
  244. return $this;
  245. }
  246. /**
  247. * Remove the specified allowed connect domain from the allowed domains.
  248. *
  249. * @param string $domain
  250. * @return $this
  251. * @since 8.1.0
  252. */
  253. public function disallowConnectDomain($domain) {
  254. $this->allowedConnectDomains = array_diff($this->allowedConnectDomains, [$domain]);
  255. return $this;
  256. }
  257. /**
  258. * From which domains media elements can be embedded.
  259. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  260. * @return $this
  261. * @since 8.1.0
  262. */
  263. public function addAllowedMediaDomain($domain) {
  264. $this->allowedMediaDomains[] = $domain;
  265. return $this;
  266. }
  267. /**
  268. * Remove the specified allowed media domain from the allowed domains.
  269. *
  270. * @param string $domain
  271. * @return $this
  272. * @since 8.1.0
  273. */
  274. public function disallowMediaDomain($domain) {
  275. $this->allowedMediaDomains = array_diff($this->allowedMediaDomains, [$domain]);
  276. return $this;
  277. }
  278. /**
  279. * From which domains objects such as <object>, <embed> or <applet> are executed
  280. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  281. * @return $this
  282. * @since 8.1.0
  283. */
  284. public function addAllowedObjectDomain($domain) {
  285. $this->allowedObjectDomains[] = $domain;
  286. return $this;
  287. }
  288. /**
  289. * Remove the specified allowed object domain from the allowed domains.
  290. *
  291. * @param string $domain
  292. * @return $this
  293. * @since 8.1.0
  294. */
  295. public function disallowObjectDomain($domain) {
  296. $this->allowedObjectDomains = array_diff($this->allowedObjectDomains, [$domain]);
  297. return $this;
  298. }
  299. /**
  300. * Which domains can be embedded in an iframe
  301. * @param string $domain Domain to whitelist. Any passed value needs to be properly sanitized.
  302. * @return $this
  303. * @since 8.1.0
  304. */
  305. public function addAllowedFrameDomain($domain) {
  306. $this->allowedFrameDomains[] = $domain;
  307. return $this;
  308. }
  309. /**
  310. * Remove the specified allowed frame domain from the allowed domains.
  311. *
  312. * @param string $domain
  313. * @return $this
  314. * @since 8.1.0
  315. */
  316. public function disallowFrameDomain($domain) {
  317. $this->allowedFrameDomains = array_diff($this->allowedFrameDomains, [$domain]);
  318. return $this;
  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. * Remove the specified allowed child src domain from the allowed domains.
  333. *
  334. * @param string $domain
  335. * @return $this
  336. * @since 8.1.0
  337. * @deprecated 15.0.0 use the WorkerSrcDomains or FrameDomain
  338. */
  339. public function disallowChildSrcDomain($domain) {
  340. $this->allowedChildSrcDomains = array_diff($this->allowedChildSrcDomains, [$domain]);
  341. return $this;
  342. }
  343. /**
  344. * Domains which can embed an iFrame of the Nextcloud instance
  345. *
  346. * @param string $domain
  347. * @return $this
  348. * @since 13.0.0
  349. */
  350. public function addAllowedFrameAncestorDomain($domain) {
  351. $this->allowedFrameAncestors[] = $domain;
  352. return $this;
  353. }
  354. /**
  355. * Domains which can embed an iFrame of the Nextcloud instance
  356. *
  357. * @param string $domain
  358. * @return $this
  359. * @since 13.0.0
  360. */
  361. public function disallowFrameAncestorDomain($domain) {
  362. $this->allowedFrameAncestors = array_diff($this->allowedFrameAncestors, [$domain]);
  363. return $this;
  364. }
  365. /**
  366. * Domain from which workers can be loaded
  367. *
  368. * @param string $domain
  369. * @return $this
  370. * @since 15.0.0
  371. */
  372. public function addAllowedWorkerSrcDomain(string $domain) {
  373. $this->allowedWorkerSrcDomains[] = $domain;
  374. return $this;
  375. }
  376. /**
  377. * Remove domain from which workers can be loaded
  378. *
  379. * @param string $domain
  380. * @return $this
  381. * @since 15.0.0
  382. */
  383. public function disallowWorkerSrcDomain(string $domain) {
  384. $this->allowedWorkerSrcDomains = array_diff($this->allowedWorkerSrcDomains, [$domain]);
  385. return $this;
  386. }
  387. /**
  388. * Domain to where forms can submit
  389. *
  390. * @since 17.0.0
  391. *
  392. * @return $this
  393. */
  394. public function addAllowedFormActionDomain(string $domain) {
  395. $this->allowedFormActionDomains[] = $domain;
  396. return $this;
  397. }
  398. /**
  399. * Remove domain to where forms can submit
  400. *
  401. * @return $this
  402. * @since 17.0.0
  403. */
  404. public function disallowFormActionDomain(string $domain) {
  405. $this->allowedFormActionDomains = array_diff($this->allowedFormActionDomains, [$domain]);
  406. return $this;
  407. }
  408. /**
  409. * Add location to report CSP violations to
  410. *
  411. * @param string $location
  412. * @return $this
  413. * @since 15.0.0
  414. */
  415. public function addReportTo(string $location) {
  416. $this->reportTo[] = $location;
  417. return $this;
  418. }
  419. /**
  420. * Get the generated Content-Security-Policy as a string
  421. * @return string
  422. * @since 8.1.0
  423. */
  424. public function buildPolicy() {
  425. $policy = "default-src 'none';";
  426. $policy .= "base-uri 'none';";
  427. $policy .= "manifest-src 'self';";
  428. if (!empty($this->allowedScriptDomains) || $this->evalScriptAllowed || $this->evalWasmAllowed) {
  429. $policy .= 'script-src ';
  430. $scriptSrc = '';
  431. if (is_string($this->jsNonce)) {
  432. if ($this->strictDynamicAllowed) {
  433. $scriptSrc .= '\'strict-dynamic\' ';
  434. }
  435. $scriptSrc .= '\'nonce-'.base64_encode($this->jsNonce).'\'';
  436. $allowedScriptDomains = array_flip($this->allowedScriptDomains);
  437. unset($allowedScriptDomains['\'self\'']);
  438. $this->allowedScriptDomains = array_flip($allowedScriptDomains);
  439. if (count($allowedScriptDomains) !== 0) {
  440. $scriptSrc .= ' ';
  441. }
  442. }
  443. if (is_array($this->allowedScriptDomains)) {
  444. $scriptSrc .= implode(' ', $this->allowedScriptDomains);
  445. }
  446. if ($this->evalScriptAllowed) {
  447. $scriptSrc .= ' \'unsafe-eval\'';
  448. }
  449. if ($this->evalWasmAllowed) {
  450. $scriptSrc .= ' \'wasm-unsafe-eval\'';
  451. }
  452. $policy .= $scriptSrc . ';';
  453. }
  454. // We only need to set this if 'strictDynamicAllowed' is not set because otherwise we can simply fall back to script-src
  455. if ($this->strictDynamicAllowedOnScripts && is_string($this->jsNonce) && !$this->strictDynamicAllowed) {
  456. $policy .= 'script-src-elem \'strict-dynamic\' ';
  457. $policy .= $scriptSrc ?? '';
  458. $policy .= ';';
  459. }
  460. if (!empty($this->allowedStyleDomains) || $this->inlineStyleAllowed) {
  461. $policy .= 'style-src ';
  462. if (is_array($this->allowedStyleDomains)) {
  463. $policy .= implode(' ', $this->allowedStyleDomains);
  464. }
  465. if ($this->inlineStyleAllowed) {
  466. $policy .= ' \'unsafe-inline\'';
  467. }
  468. $policy .= ';';
  469. }
  470. if (!empty($this->allowedImageDomains)) {
  471. $policy .= 'img-src ' . implode(' ', $this->allowedImageDomains);
  472. $policy .= ';';
  473. }
  474. if (!empty($this->allowedFontDomains)) {
  475. $policy .= 'font-src ' . implode(' ', $this->allowedFontDomains);
  476. $policy .= ';';
  477. }
  478. if (!empty($this->allowedConnectDomains)) {
  479. $policy .= 'connect-src ' . implode(' ', $this->allowedConnectDomains);
  480. $policy .= ';';
  481. }
  482. if (!empty($this->allowedMediaDomains)) {
  483. $policy .= 'media-src ' . implode(' ', $this->allowedMediaDomains);
  484. $policy .= ';';
  485. }
  486. if (!empty($this->allowedObjectDomains)) {
  487. $policy .= 'object-src ' . implode(' ', $this->allowedObjectDomains);
  488. $policy .= ';';
  489. }
  490. if (!empty($this->allowedFrameDomains)) {
  491. $policy .= 'frame-src ';
  492. $policy .= implode(' ', $this->allowedFrameDomains);
  493. $policy .= ';';
  494. }
  495. if (!empty($this->allowedChildSrcDomains)) {
  496. $policy .= 'child-src ' . implode(' ', $this->allowedChildSrcDomains);
  497. $policy .= ';';
  498. }
  499. if (!empty($this->allowedFrameAncestors)) {
  500. $policy .= 'frame-ancestors ' . implode(' ', $this->allowedFrameAncestors);
  501. $policy .= ';';
  502. } else {
  503. $policy .= 'frame-ancestors \'none\';';
  504. }
  505. if (!empty($this->allowedWorkerSrcDomains)) {
  506. $policy .= 'worker-src ' . implode(' ', $this->allowedWorkerSrcDomains);
  507. $policy .= ';';
  508. }
  509. if (!empty($this->allowedFormActionDomains)) {
  510. $policy .= 'form-action ' . implode(' ', $this->allowedFormActionDomains);
  511. $policy .= ';';
  512. }
  513. if (!empty($this->reportTo)) {
  514. $policy .= 'report-uri ' . implode(' ', $this->reportTo);
  515. $policy .= ';';
  516. }
  517. return rtrim($policy, ';');
  518. }
  519. }