5 use BookStack\Util\CspService;
6 use Illuminate\Testing\TestResponse;
8 class SecurityHeaderTest extends TestCase
10 public function test_cookies_samesite_lax_by_default()
12 $resp = $this->get('/');
13 foreach ($resp->headers->getCookies() as $cookie) {
14 $this->assertEquals('lax', $cookie->getSameSite());
18 public function test_cookies_samesite_none_when_iframe_hosts_set()
20 $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'http://example.com', function () {
21 $resp = $this->get('/');
22 foreach ($resp->headers->getCookies() as $cookie) {
23 $this->assertEquals('none', $cookie->getSameSite());
28 public function test_secure_cookies_controlled_by_app_url()
30 $this->runWithEnv('APP_URL', 'http://example.com', function () {
31 $resp = $this->get('/');
32 foreach ($resp->headers->getCookies() as $cookie) {
33 $this->assertFalse($cookie->isSecure());
37 $this->runWithEnv('APP_URL', 'https://example.com', function () {
38 $resp = $this->get('/');
39 foreach ($resp->headers->getCookies() as $cookie) {
40 $this->assertTrue($cookie->isSecure());
45 public function test_iframe_csp_self_only_by_default()
47 $resp = $this->get('/');
48 $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
50 $this->assertEquals('frame-ancestors \'self\'', $frameHeader);
53 public function test_iframe_csp_includes_extra_hosts_if_configured()
55 $this->runWithEnv('ALLOWED_IFRAME_HOSTS', 'https://a.example.com https://b.example.com', function () {
56 $resp = $this->get('/');
57 $frameHeader = $this->getCspHeader($resp, 'frame-ancestors');
59 $this->assertNotEmpty($frameHeader);
60 $this->assertEquals('frame-ancestors \'self\' https://a.example.com https://b.example.com', $frameHeader);
64 public function test_script_csp_set_on_responses()
66 $resp = $this->get('/');
67 $scriptHeader = $this->getCspHeader($resp, 'script-src');
68 $this->assertStringContainsString('\'strict-dynamic\'', $scriptHeader);
69 $this->assertStringContainsString('\'nonce-', $scriptHeader);
72 public function test_script_csp_nonce_matches_nonce_used_in_custom_head()
74 $this->setSettings(['app-custom-head' => '<script>console.log("cat");</script>']);
75 $resp = $this->get('/login');
76 $scriptHeader = $this->getCspHeader($resp, 'script-src');
78 $nonce = app()->make(CspService::class)->getNonce();
79 $this->assertStringContainsString('nonce-' . $nonce, $scriptHeader);
80 $resp->assertSee('<script nonce="' . $nonce . '">console.log("cat");</script>', false);
83 public function test_script_csp_nonce_changes_per_request()
85 $resp = $this->get('/');
86 $firstHeader = $this->getCspHeader($resp, 'script-src');
88 $this->refreshApplication();
90 $resp = $this->get('/');
91 $secondHeader = $this->getCspHeader($resp, 'script-src');
93 $this->assertNotEquals($firstHeader, $secondHeader);
96 public function test_allow_content_scripts_settings_controls_csp_script_headers()
98 config()->set('app.allow_content_scripts', true);
99 $resp = $this->get('/');
100 $scriptHeader = $this->getCspHeader($resp, 'script-src');
101 $this->assertEmpty($scriptHeader);
103 config()->set('app.allow_content_scripts', false);
104 $resp = $this->get('/');
105 $scriptHeader = $this->getCspHeader($resp, 'script-src');
106 $this->assertNotEmpty($scriptHeader);
109 public function test_object_src_csp_header_set()
111 $resp = $this->get('/');
112 $scriptHeader = $this->getCspHeader($resp, 'object-src');
113 $this->assertEquals('object-src \'self\'', $scriptHeader);
116 public function test_base_uri_csp_header_set()
118 $resp = $this->get('/');
119 $scriptHeader = $this->getCspHeader($resp, 'base-uri');
120 $this->assertEquals('base-uri \'self\'', $scriptHeader);
123 public function test_frame_src_csp_header_set()
125 $resp = $this->get('/');
126 $scriptHeader = $this->getCspHeader($resp, 'frame-src');
127 $this->assertEquals('frame-src \'self\' https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com', $scriptHeader);
130 public function test_frame_src_csp_header_has_drawio_host_added()
133 'app.iframe_sources' => 'https://example.com',
134 'services.drawio' => 'https://diagrams.example.com/testing?cat=dog',
137 $resp = $this->get('/');
138 $scriptHeader = $this->getCspHeader($resp, 'frame-src');
139 $this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com', $scriptHeader);
142 public function test_frame_src_csp_header_drawio_host_includes_port_if_existing()
145 'app.iframe_sources' => 'https://example.com',
146 'services.drawio' => 'https://diagrams.example.com:8080/testing?cat=dog',
149 $resp = $this->get('/');
150 $scriptHeader = $this->getCspHeader($resp, 'frame-src');
151 $this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com:8080', $scriptHeader);
154 public function test_cache_control_headers_are_set_on_responses()
157 $resp = $this->get('/');
158 $resp->assertHeader('Cache-Control', 'no-cache, no-store, private');
159 $resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
163 $resp = $this->get('/');
164 $resp->assertHeader('Cache-Control', 'no-cache, no-store, private');
165 $resp->assertHeader('Expires', 'Sun, 12 Jul 2015 19:01:00 GMT');
169 * Get the value of the first CSP header of the given type.
171 protected function getCspHeader(TestResponse $resp, string $type): string
173 $cspHeaders = explode('; ', $resp->headers->get('Content-Security-Policy'));
175 foreach ($cspHeaders as $cspHeader) {
176 if (strpos($cspHeader, $type) === 0) {