X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/492af79c27f089e28c76007f93fef4995eda9d94..refs/pull/3693/head:/app/Util/CspService.php diff --git a/app/Util/CspService.php b/app/Util/CspService.php index 2979ebc3e..f9ab666ac 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -3,16 +3,14 @@ namespace BookStack\Util; use Illuminate\Support\Str; -use Symfony\Component\HttpFoundation\Response; class CspService { - /** @var string */ - protected $nonce; + protected string $nonce; public function __construct(string $nonce = '') { - $this->nonce = $nonce ?: Str::random(16); + $this->nonce = $nonce ?: Str::random(24); } /** @@ -24,13 +22,51 @@ class CspService } /** - * Sets CSP 'script-src' headers to restrict the forms of script that can - * run on the page. + * Get the CSP headers for the application. */ - public function setScriptSrc(Response $response) + public function getCspHeader(): string + { + $headers = [ + $this->getFrameAncestors(), + $this->getFrameSrc(), + $this->getScriptSrc(), + $this->getObjectSrc(), + $this->getBaseUri(), + ]; + + return implode('; ', array_filter($headers)); + } + + /** + * Get the CSP rules for the application for a HTML meta tag. + */ + public function getCspMetaTagValue(): string + { + $headers = [ + $this->getFrameSrc(), + $this->getScriptSrc(), + $this->getObjectSrc(), + $this->getBaseUri(), + ]; + + return implode('; ', array_filter($headers)); + } + + /** + * Check if the user has configured some allowed iframe hosts. + */ + public function allowedIFrameHostsConfigured(): bool + { + return count($this->getAllowedIframeHosts()) > 0; + } + + /** + * Create CSP 'script-src' rule to restrict the forms of script that can run on the page. + */ + protected function getScriptSrc(): string { if (config('app.allow_content_scripts')) { - return; + return ''; } $parts = [ @@ -40,57 +76,75 @@ class CspService '\'strict-dynamic\'', ]; - $value = 'script-src ' . implode(' ', $parts); - $response->headers->set('Content-Security-Policy', $value, false); + return 'script-src ' . implode(' ', $parts); } /** - * Sets CSP "frame-ancestors" headers to restrict the hosts that BookStack can be - * iframed within. Also adjusts the cookie samesite options so that cookies will - * operate in the third-party context. + * Create CSP "frame-ancestors" rule to restrict the hosts that BookStack can be iframed within. */ - public function setFrameAncestors(Response $response) + protected function getFrameAncestors(): string { $iframeHosts = $this->getAllowedIframeHosts(); array_unshift($iframeHosts, "'self'"); - $cspValue = 'frame-ancestors ' . implode(' ', $iframeHosts); - $response->headers->set('Content-Security-Policy', $cspValue, false); + + return 'frame-ancestors ' . implode(' ', $iframeHosts); } /** - * Check if the user has configured some allowed iframe hosts. + * Creates CSP "frame-src" rule to restrict what hosts/sources can be loaded + * within iframes to provide an allow-list-style approach to iframe content. */ - public function allowedIFrameHostsConfigured(): bool + protected function getFrameSrc(): string { - return count($this->getAllowedIframeHosts()) > 0; + $iframeHosts = $this->getAllowedIframeSources(); + array_unshift($iframeHosts, "'self'"); + + return 'frame-src ' . implode(' ', $iframeHosts); } /** - * Sets CSP 'object-src' headers to restrict the types of dynamic content + * Creates CSP 'object-src' rule to restrict the types of dynamic content * that can be embedded on the page. */ - public function setObjectSrc(Response $response) + protected function getObjectSrc(): string { if (config('app.allow_content_scripts')) { - return; + return ''; } - $response->headers->set('Content-Security-Policy', 'object-src \'self\'', false); + return "object-src 'self'"; } /** - * Sets CSP 'base-uri' headers to restrict what base tags can be set on + * Creates CSP 'base-uri' rule to restrict what base tags can be set on * the page to prevent manipulation of relative links. */ - public function setBaseUri(Response $response) + protected function getBaseUri(): string { - $response->headers->set('Content-Security-Policy', 'base-uri \'self\'', false); + return "base-uri 'self'"; } protected function getAllowedIframeHosts(): array { $hosts = config('app.iframe_hosts', ''); + return array_filter(explode(' ', $hosts)); } -} \ No newline at end of file + protected function getAllowedIframeSources(): array + { + $sources = config('app.iframe_sources', ''); + $hosts = array_filter(explode(' ', $sources)); + + // Extract drawing service url to allow embedding if active + $drawioConfigValue = config('services.drawio'); + if ($drawioConfigValue) { + $drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/'; + $drawioSourceParsed = parse_url($drawioSource); + $drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host']; + $hosts[] = $drawioHost; + } + + return $hosts; + } +}