]> BookStack Code Mirror - bookstack/blob - app/Util/CspService.php
Updated CSP with frame-src rules
[bookstack] / app / Util / CspService.php
1 <?php
2
3 namespace BookStack\Util;
4
5 use Illuminate\Support\Str;
6
7 class CspService
8 {
9     protected string $nonce;
10
11     public function __construct(string $nonce = '')
12     {
13         $this->nonce = $nonce ?: Str::random(24);
14     }
15
16     /**
17      * Get the nonce value for CSP.
18      */
19     public function getNonce(): string
20     {
21         return $this->nonce;
22     }
23
24     /**
25      * Get the CSP headers for the application
26      */
27     public function getCspHeader(): string
28     {
29         $headers = [
30             $this->getFrameAncestors(),
31             $this->getFrameSrc(),
32             $this->getScriptSrc(),
33             $this->getObjectSrc(),
34             $this->getBaseUri(),
35         ];
36
37         return implode('; ', array_filter($headers));
38     }
39
40     /**
41      * Get the CSP rules for the application for a HTML meta tag.
42      */
43     public function getCspMetaTagValue(): string
44     {
45         $headers = [
46             $this->getFrameSrc(),
47             $this->getScriptSrc(),
48             $this->getObjectSrc(),
49             $this->getBaseUri(),
50         ];
51
52         return implode('; ', array_filter($headers));
53     }
54
55     /**
56      * Check if the user has configured some allowed iframe hosts.
57      */
58     public function allowedIFrameHostsConfigured(): bool
59     {
60         return count($this->getAllowedIframeHosts()) > 0;
61     }
62
63     /**
64      * Create CSP 'script-src' rule to restrict the forms of script that can run on the page.
65      */
66     protected function getScriptSrc(): string
67     {
68         if (config('app.allow_content_scripts')) {
69             return '';
70         }
71
72         $parts = [
73             'http:',
74             'https:',
75             '\'nonce-' . $this->nonce . '\'',
76             '\'strict-dynamic\'',
77         ];
78
79         return 'script-src ' . implode(' ', $parts);
80     }
81
82     /**
83      * Create CSP "frame-ancestors" rule to restrict the hosts that BookStack can be iframed within.
84      */
85     protected function getFrameAncestors(): string
86     {
87         $iframeHosts = $this->getAllowedIframeHosts();
88         array_unshift($iframeHosts, "'self'");
89         return 'frame-ancestors ' . implode(' ', $iframeHosts);
90     }
91
92     /**
93      * Creates CSP "frame-src" rule to restrict what hosts/sources can be loaded
94      * within iframes to provide an allow-list-style approach to iframe content.
95      */
96     protected function getFrameSrc(): string
97     {
98         $iframeHosts = $this->getAllowedIframeSources();
99         array_unshift($iframeHosts, "'self'");
100         return 'frame-src ' . implode(' ', $iframeHosts);
101     }
102
103     /**
104      * Creates CSP 'object-src' rule to restrict the types of dynamic content
105      * that can be embedded on the page.
106      */
107     protected function getObjectSrc(): string
108     {
109         if (config('app.allow_content_scripts')) {
110             return '';
111         }
112
113         return "object-src 'self'";
114     }
115
116     /**
117      * Creates CSP 'base-uri' rule to restrict what base tags can be set on
118      * the page to prevent manipulation of relative links.
119      */
120     protected function getBaseUri(): string
121     {
122         return "base-uri 'self'";
123     }
124
125     protected function getAllowedIframeHosts(): array
126     {
127         $hosts = config('app.iframe_hosts', '');
128
129         return array_filter(explode(' ', $hosts));
130     }
131
132     protected function getAllowedIframeSources(): array
133     {
134         $sources = config('app.iframe_sources', '');
135         $hosts = array_filter(explode(' ', $sources));
136
137         // Extract drawing service url to allow embedding if active
138         $drawioConfigValue = config('services.drawio');
139         if ($drawioConfigValue) {
140             $drawioSource = is_string($drawioConfigValue) ? $drawioConfigValue : 'https://embed.diagrams.net/';
141             $drawioSourceParsed = parse_url($drawioSource);
142             $drawioHost = $drawioSourceParsed['scheme'] . '://' . $drawioSourceParsed['host'];
143             $hosts[] = $drawioHost;
144         }
145
146         return $hosts;
147     }
148 }