]> BookStack Code Mirror - bookstack/blobdiff - app/Util/CspService.php
respective book and chapter structure added.
[bookstack] / app / Util / CspService.php
index 2979ebc3e1b2c3a5a793d6cd06666c73361840dd..4262b5c98f8071828eacc073d92d9522b7beabbe 100644 (file)
@@ -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,87 @@ 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', '');
+        $hosts = config('app.iframe_hosts') ?? '';
+
         return array_filter(explode(' ', $hosts));
     }
 
-}
\ No newline at end of file
+    protected function getAllowedIframeSources(): array
+    {
+        $sources = explode(' ', config('app.iframe_sources', ''));
+        $sources[] = $this->getDrawioHost();
+
+        return array_filter($sources);
+    }
+
+    /**
+     * Extract the host name of the configured drawio URL for use in CSP.
+     * Returns empty string if not in use.
+     */
+    protected function getDrawioHost(): string
+    {
+        $drawioConfigValue = config('services.drawio');
+        if (!$drawioConfigValue) {
+            return '';
+        }
+
+        $drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/';
+        $drawioSourceParsed = parse_url($drawioSource);
+        $drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host'];
+        if (isset($drawioSourceParsed['port'])) {
+            $drawioHost .= ':' . $drawioSourceParsed['port'];
+        }
+
+        return $drawioHost;
+    }
+}