Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .env.example.complete
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,18 @@ ALLOWED_IFRAME_HOSTS=null
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"

# A list of sources/hostnames that can be loaded as CSS styles within BookStack.
# Space separated if multiple. BookStack host domain is auto-inferred.
# Defaults to a permissive set if not provided.
# Example: ALLOWED_CSS_SOURCES="https://fonts.googleapis.com"
ALLOWED_CSS_SOURCES=null

# A list of sources/hostnames that can be loaded as image content within BookStack.
# Space separated if multiple. BookStack host domain is auto-inferred.
# Defaults to a permissive set if not provided.
# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com data:"
ALLOWED_IMAGE_SOURCES=null

# A list of the sources/hostnames that can be reached by application SSR calls.
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
# Host-specific functionality (usually controlled via other options) like auth
Expand Down
10 changes: 10 additions & 0 deletions app/Config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,16 @@
// Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'),

// A list of sources/hostnames that can be loaded as CSS styles within BookStack.
// Space separated if multiple. BookStack host domain is auto-inferred.
// If not set, a permissive default set is used to reduce potential breakage.
'css_sources' => env('ALLOWED_CSS_SOURCES', null),

// A list of sources/hostnames that can be loaded as image content within BookStack.
// Space separated if multiple. BookStack host domain is auto-inferred.
// If not set, a permissive default set is used to reduce potential breakage.
'image_sources' => env('ALLOWED_IMAGE_SOURCES', null),

// A list of the sources/hostnames that can be reached by application SSR calls.
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
// Host-specific functionality (usually controlled via other options) like auth
Expand Down
65 changes: 65 additions & 0 deletions app/Util/CspService.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public function getCspHeader(): string
$this->getFrameAncestors(),
$this->getFrameSrc(),
$this->getScriptSrc(),
$this->getStyleSrc(),
$this->getImgSrc(),
$this->getObjectSrc(),
$this->getBaseUri(),
];
Expand All @@ -45,6 +47,8 @@ public function getCspMetaTagValue(): string
$headers = [
$this->getFrameSrc(),
$this->getScriptSrc(),
$this->getStyleSrc(),
$this->getImgSrc(),
$this->getObjectSrc(),
$this->getBaseUri(),
];
Expand Down Expand Up @@ -115,6 +119,22 @@ protected function getObjectSrc(): string
return "object-src 'self'";
}

/**
* Creates CSP 'style-src' rule to restrict where styles can be loaded from.
*/
protected function getStyleSrc(): string
{
return 'style-src ' . implode(' ', $this->getAllowedStyleSources());
}

/**
* Creates CSP 'img-src' rule to restrict where images can be loaded from.
*/
protected function getImgSrc(): string
{
return 'img-src ' . implode(' ', $this->getAllowedImageSources());
}

/**
* Creates CSP 'base-uri' rule to restrict what base tags can be set on
* the page to prevent manipulation of relative links.
Expand Down Expand Up @@ -144,6 +164,51 @@ protected function getAllowedIframeSources(): array
return array_filter($sources);
}

/**
* Get allowed style sources for the style-src directive.
*/
protected function getAllowedStyleSources(): array
{
$configured = config('app.css_sources');

if (is_string($configured)) {
$sources = array_filter(explode(' ', $configured));
array_unshift($sources, "'self'");

return array_values(array_unique($sources));
}

return [
"'self'",
"'unsafe-inline'",
'http:',
'https:',
];
}

/**
* Get allowed image sources for the img-src directive.
*/
protected function getAllowedImageSources(): array
{
$configured = config('app.image_sources');

if (is_string($configured)) {
$sources = array_filter(explode(' ', $configured));
array_unshift($sources, "'self'");

return array_values(array_unique($sources));
}

return [
"'self'",
'data:',
'blob:',
'http:',
'https:',
];
}

/**
* Extract the host name of the configured drawio URL for use in CSP.
* Returns empty string if not in use.
Expand Down
42 changes: 42 additions & 0 deletions dev/docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,48 @@ BookStack has a large suite of PHP tests to cover application functionality. We

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

## Content Security Policy Controls

BookStack enforces a Content Security Policy (CSP) response header to reduce risk from injected content and untrusted embeds.

For backward compatibility, image and CSS controls are intentionally permissive by default, but can be tightened via environment options.

### Related Environment Options

These values are defined in `.env.example.complete`:

- `ALLOWED_CSS_SOURCES`
- Controls allowed `style-src` sources.
- Defaults to a permissive fallback if unset.
- `ALLOWED_IMAGE_SOURCES`
- Controls allowed `img-src` sources.
- Defaults to a permissive fallback if unset.

Values should be space-separated source expressions.

### Example Configurations

Allow Google Fonts CSS and local styles only:

```bash
ALLOWED_CSS_SOURCES="https://fonts.googleapis.com"
```

Allow local images, embedded data images, and a dedicated image CDN:

```bash
ALLOWED_IMAGE_SOURCES="data: https://images.example.com"
```

### Tightening Guidance

When hardening a deployment:

1. Start with defaults to avoid unexpected breakage.
2. Set explicit `ALLOWED_CSS_SOURCES` and `ALLOWED_IMAGE_SOURCES` values for the domains you actually use.
3. Test key workflows (editor, page display, theme assets, external embeds) and browser console CSP warnings.
4. Remove unnecessary protocols and hosts over time.

## Code Standards

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.
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ Big thanks to these companies for supporting the project.
## 🛠️ Development & Testing

Please see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code.
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).

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.

Expand Down
36 changes: 36 additions & 0 deletions tests/SecurityHeaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,42 @@ public function test_frame_src_csp_header_drawio_host_includes_port_if_existing(
$this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com:8080', $scriptHeader);
}

public function test_style_src_csp_header_set_to_permissive_defaults_when_not_configured()
{
$resp = $this->get('/');
$header = $this->getCspHeader($resp, 'style-src');

$this->assertEquals("style-src 'self' 'unsafe-inline' http: https:", $header);
}

public function test_style_src_csp_header_can_be_overridden_by_config()
{
config()->set('app.css_sources', 'https://fonts.example.com');

$resp = $this->get('/');
$header = $this->getCspHeader($resp, 'style-src');

$this->assertEquals("style-src 'self' https://fonts.example.com", $header);
}

public function test_img_src_csp_header_set_to_permissive_defaults_when_not_configured()
{
$resp = $this->get('/');
$header = $this->getCspHeader($resp, 'img-src');

$this->assertEquals("img-src 'self' data: blob: http: https:", $header);
}

public function test_img_src_csp_header_can_be_overridden_by_config()
{
config()->set('app.image_sources', 'https://images.example.com data:');

$resp = $this->get('/');
$header = $this->getCspHeader($resp, 'img-src');

$this->assertEquals("img-src 'self' https://images.example.com data:", $header);
}

public function test_cache_control_headers_are_set_on_responses()
{
// Public access
Expand Down