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 = '')
{
}
/**
- * 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 = [
'\'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));
}
+
+ 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;
+ }
}