Skip to content

Commit e42fda8

Browse files
committed
Add CSP controls for image and CSS sources
1 parent 25790fd commit e42fda8

File tree

6 files changed

+166
-0
lines changed

6 files changed

+166
-0
lines changed

.env.example.complete

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,18 @@ ALLOWED_IFRAME_HOSTS=null
395395
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
396396
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
397397

398+
# A list of sources/hostnames that can be loaded as CSS styles within BookStack.
399+
# Space separated if multiple. BookStack host domain is auto-inferred.
400+
# Defaults to a permissive set if not provided.
401+
# Example: ALLOWED_CSS_SOURCES="https://fonts.googleapis.com"
402+
ALLOWED_CSS_SOURCES=null
403+
404+
# A list of sources/hostnames that can be loaded as image content within BookStack.
405+
# Space separated if multiple. BookStack host domain is auto-inferred.
406+
# Defaults to a permissive set if not provided.
407+
# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com data:"
408+
ALLOWED_IMAGE_SOURCES=null
409+
398410
# A list of the sources/hostnames that can be reached by application SSR calls.
399411
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
400412
# Host-specific functionality (usually controlled via other options) like auth

app/Config/app.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
7373
'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),
7474

75+
// A list of sources/hostnames that can be loaded as CSS styles within BookStack.
76+
// Space separated if multiple. BookStack host domain is auto-inferred.
77+
// If not set, a permissive default set is used to reduce potential breakage.
78+
'css_sources' => env('ALLOWED_CSS_SOURCES', null),
79+
80+
// A list of sources/hostnames that can be loaded as image content within BookStack.
81+
// Space separated if multiple. BookStack host domain is auto-inferred.
82+
// If not set, a permissive default set is used to reduce potential breakage.
83+
'image_sources' => env('ALLOWED_IMAGE_SOURCES', null),
84+
7585
// A list of the sources/hostnames that can be reached by application SSR calls.
7686
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
7787
// Host-specific functionality (usually controlled via other options) like auth

app/Util/CspService.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public function getCspHeader(): string
3030
$this->getFrameAncestors(),
3131
$this->getFrameSrc(),
3232
$this->getScriptSrc(),
33+
$this->getStyleSrc(),
34+
$this->getImgSrc(),
3335
$this->getObjectSrc(),
3436
$this->getBaseUri(),
3537
];
@@ -45,6 +47,8 @@ public function getCspMetaTagValue(): string
4547
$headers = [
4648
$this->getFrameSrc(),
4749
$this->getScriptSrc(),
50+
$this->getStyleSrc(),
51+
$this->getImgSrc(),
4852
$this->getObjectSrc(),
4953
$this->getBaseUri(),
5054
];
@@ -115,6 +119,22 @@ protected function getObjectSrc(): string
115119
return "object-src 'self'";
116120
}
117121

122+
/**
123+
* Creates CSP 'style-src' rule to restrict where styles can be loaded from.
124+
*/
125+
protected function getStyleSrc(): string
126+
{
127+
return 'style-src ' . implode(' ', $this->getAllowedStyleSources());
128+
}
129+
130+
/**
131+
* Creates CSP 'img-src' rule to restrict where images can be loaded from.
132+
*/
133+
protected function getImgSrc(): string
134+
{
135+
return 'img-src ' . implode(' ', $this->getAllowedImageSources());
136+
}
137+
118138
/**
119139
* Creates CSP 'base-uri' rule to restrict what base tags can be set on
120140
* the page to prevent manipulation of relative links.
@@ -144,6 +164,51 @@ protected function getAllowedIframeSources(): array
144164
return array_filter($sources);
145165
}
146166

167+
/**
168+
* Get allowed style sources for the style-src directive.
169+
*/
170+
protected function getAllowedStyleSources(): array
171+
{
172+
$configured = config('app.css_sources');
173+
174+
if (is_string($configured)) {
175+
$sources = array_filter(explode(' ', $configured));
176+
array_unshift($sources, "'self'");
177+
178+
return array_values(array_unique($sources));
179+
}
180+
181+
return [
182+
"'self'",
183+
"'unsafe-inline'",
184+
'http:',
185+
'https:',
186+
];
187+
}
188+
189+
/**
190+
* Get allowed image sources for the img-src directive.
191+
*/
192+
protected function getAllowedImageSources(): array
193+
{
194+
$configured = config('app.image_sources');
195+
196+
if (is_string($configured)) {
197+
$sources = array_filter(explode(' ', $configured));
198+
array_unshift($sources, "'self'");
199+
200+
return array_values(array_unique($sources));
201+
}
202+
203+
return [
204+
"'self'",
205+
'data:',
206+
'blob:',
207+
'http:',
208+
'https:',
209+
];
210+
}
211+
147212
/**
148213
* Extract the host name of the configured drawio URL for use in CSP.
149214
* Returns empty string if not in use.

dev/docs/development.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,48 @@ BookStack has a large suite of PHP tests to cover application functionality. We
3131

3232
For details about setting-up, running and writing tests please see the [php-testing.md document](php-testing.md).
3333

34+
## Content Security Policy Controls
35+
36+
BookStack enforces a Content Security Policy (CSP) response header to reduce risk from injected content and untrusted embeds.
37+
38+
For backward compatibility, image and CSS controls are intentionally permissive by default, but can be tightened via environment options.
39+
40+
### Related Environment Options
41+
42+
These values are defined in `.env.example.complete`:
43+
44+
- `ALLOWED_CSS_SOURCES`
45+
- Controls allowed `style-src` sources.
46+
- Defaults to a permissive fallback if unset.
47+
- `ALLOWED_IMAGE_SOURCES`
48+
- Controls allowed `img-src` sources.
49+
- Defaults to a permissive fallback if unset.
50+
51+
Values should be space-separated source expressions.
52+
53+
### Example Configurations
54+
55+
Allow Google Fonts CSS and local styles only:
56+
57+
```bash
58+
ALLOWED_CSS_SOURCES="https://fonts.googleapis.com"
59+
```
60+
61+
Allow local images, embedded data images, and a dedicated image CDN:
62+
63+
```bash
64+
ALLOWED_IMAGE_SOURCES="data: https://images.example.com"
65+
```
66+
67+
### Tightening Guidance
68+
69+
When hardening a deployment:
70+
71+
1. Start with defaults to avoid unexpected breakage.
72+
2. Set explicit `ALLOWED_CSS_SOURCES` and `ALLOWED_IMAGE_SOURCES` values for the domains you actually use.
73+
3. Test key workflows (editor, page display, theme assets, external embeds) and browser console CSP warnings.
74+
4. Remove unnecessary protocols and hosts over time.
75+
3476
## Code Standards
3577

3678
We use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools.

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Big thanks to these companies for supporting the project.
102102
## 🛠️ Development & Testing
103103

104104
Please see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code.
105+
For details on Content Security Policy controls (including image and CSS source options), see the **Content Security Policy Controls** section in the [development docs](dev/docs/development.md).
105106

106107
If you're just looking to customize or extend your own BookStack instance, take a look at our [Hacking BookStack documentation page](https://www.bookstackapp.com/docs/admin/hacking-bookstack/) for details on various options to achieve this without altering the BookStack source code.
107108

tests/SecurityHeaderTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,42 @@ public function test_frame_src_csp_header_drawio_host_includes_port_if_existing(
151151
$this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com:8080', $scriptHeader);
152152
}
153153

154+
public function test_style_src_csp_header_set_to_permissive_defaults_when_not_configured()
155+
{
156+
$resp = $this->get('/');
157+
$header = $this->getCspHeader($resp, 'style-src');
158+
159+
$this->assertEquals("style-src 'self' 'unsafe-inline' http: https:", $header);
160+
}
161+
162+
public function test_style_src_csp_header_can_be_overridden_by_config()
163+
{
164+
config()->set('app.css_sources', 'https://fonts.example.com');
165+
166+
$resp = $this->get('/');
167+
$header = $this->getCspHeader($resp, 'style-src');
168+
169+
$this->assertEquals("style-src 'self' https://fonts.example.com", $header);
170+
}
171+
172+
public function test_img_src_csp_header_set_to_permissive_defaults_when_not_configured()
173+
{
174+
$resp = $this->get('/');
175+
$header = $this->getCspHeader($resp, 'img-src');
176+
177+
$this->assertEquals("img-src 'self' data: blob: http: https:", $header);
178+
}
179+
180+
public function test_img_src_csp_header_can_be_overridden_by_config()
181+
{
182+
config()->set('app.image_sources', 'https://images.example.com data:');
183+
184+
$resp = $this->get('/');
185+
$header = $this->getCspHeader($resp, 'img-src');
186+
187+
$this->assertEquals("img-src 'self' https://images.example.com data:", $header);
188+
}
189+
154190
public function test_cache_control_headers_are_set_on_responses()
155191
{
156192
// Public access

0 commit comments

Comments
 (0)