]> BookStack Code Mirror - bookstack/blob - tests/Entity/PageContentTest.php
Hardened page content script escaping
[bookstack] / tests / Entity / PageContentTest.php
1 <?php namespace Tests;
2
3 use BookStack\Entities\Page;
4 use BookStack\Entities\Repos\EntityRepo;
5 use BookStack\Entities\Repos\PageRepo;
6
7 class PageContentTest extends TestCase
8 {
9
10     public function test_page_includes()
11     {
12         $page = Page::first();
13         $secondPage = Page::where('id', '!=', $page->id)->first();
14
15         $secondPage->html = "<p id='section1'>Hello, This is a test</p><p id='section2'>This is a second block of content</p>";
16         $secondPage->save();
17
18         $this->asEditor();
19
20         $pageContent = $this->get($page->getUrl());
21         $pageContent->assertDontSee('Hello, This is a test');
22
23         $originalHtml = $page->html;
24         $page->html .= "{{@{$secondPage->id}}}";
25         $page->save();
26
27         $pageContent = $this->get($page->getUrl());
28         $pageContent->assertSee('Hello, This is a test');
29         $pageContent->assertSee('This is a second block of content');
30
31         $page->html = $originalHtml . " Well {{@{$secondPage->id}#section2}}";
32         $page->save();
33
34         $pageContent = $this->get($page->getUrl());
35         $pageContent->assertDontSee('Hello, This is a test');
36         $pageContent->assertSee('Well This is a second block of content');
37     }
38
39     public function test_saving_page_with_includes()
40     {
41         $page = Page::first();
42         $secondPage = Page::where('id', '!=', $page->id)->first();
43
44         $this->asEditor();
45         $includeTag = '{{@' . $secondPage->id . '}}';
46         $page->html = '<p>' . $includeTag . '</p>';
47
48         $resp = $this->put($page->getUrl(), ['name' => $page->name, 'html' => $page->html, 'summary' => '']);
49
50         $resp->assertStatus(302);
51
52         $page = Page::find($page->id);
53         $this->assertContains($includeTag, $page->html);
54         $this->assertEquals('', $page->text);
55     }
56
57     public function test_page_includes_do_not_break_tables()
58     {
59         $page = Page::first();
60         $secondPage = Page::where('id', '!=', $page->id)->first();
61
62         $content = '<table id="table"><tbody><tr><td>test</td></tr></tbody></table>';
63         $secondPage->html = $content;
64         $secondPage->save();
65
66         $page->html = "{{@{$secondPage->id}#table}}";
67         $page->save();
68
69         $this->asEditor();
70         $pageResp = $this->get($page->getUrl());
71         $pageResp->assertSee($content);
72     }
73
74     public function test_page_content_scripts_removed_by_default()
75     {
76         $this->asEditor();
77         $page = Page::first();
78         $script = 'abc123<script>console.log("hello-test")</script>abc123';
79         $page->html = "escape {$script}";
80         $page->save();
81
82         $pageView = $this->get($page->getUrl());
83         $pageView->assertDontSee($script);
84         $pageView->assertSee('abc123abc123');
85     }
86
87     public function test_more_complex_content_script_escaping_scenarios()
88     {
89         $checks = [
90             "<p>Some script</p><script>alert('cat')</script>",
91             "<div><div><div><div><p>Some script</p><script>alert('cat')</script></div></div></div></div>",
92             "<p>Some script<script>alert('cat')</script></p>",
93             "<p>Some script <div><script>alert('cat')</script></div></p>",
94             "<p>Some script <script><div>alert('cat')</script></div></p>",
95             "<p>Some script <script><div>alert('cat')</script><script><div>alert('cat')</script></p><script><div>alert('cat')</script>",
96         ];
97
98         $this->asEditor();
99         $page = Page::first();
100
101         foreach ($checks as $check) {
102             $page->html = $check;
103             $page->save();
104
105             $pageView = $this->get($page->getUrl());
106             $pageView->assertElementNotContains('.page-content', '<script>');
107             $pageView->assertElementNotContains('.page-content', '</script>');
108         }
109
110     }
111
112     public function test_page_inline_on_attributes_removed_by_default()
113     {
114         $this->asEditor();
115         $page = Page::first();
116         $script = '<p onmouseenter="console.log(\'test\')">Hello</p>';
117         $page->html = "escape {$script}";
118         $page->save();
119
120         $pageView = $this->get($page->getUrl());
121         $pageView->assertDontSee($script);
122         $pageView->assertSee('<p>Hello</p>');
123     }
124
125     public function test_more_complex_inline_on_attributes_escaping_scenarios()
126     {
127         $checks = [
128             '<p onclick="console.log(\'test\')">Hello</p>',
129             '<div>Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p>',
130             '<div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div>',
131             '<div><div><div><div>Lorem ipsum dolor sit amet.<p onclick="console.log(\'test\')">Hello</p></div></div></div></div>',
132             '<div onclick="console.log(\'test\')">Lorem ipsum dolor sit amet.</div><p onclick="console.log(\'test\')">Hello</p><div></div>',
133         ];
134
135         $this->asEditor();
136         $page = Page::first();
137
138         foreach ($checks as $check) {
139             $page->html = $check;
140             $page->save();
141
142             $pageView = $this->get($page->getUrl());
143             $pageView->assertElementNotContains('.page-content', 'onclick');
144         }
145
146     }
147
148     public function test_page_content_scripts_show_when_configured()
149     {
150         $this->asEditor();
151         $page = Page::first();
152         config()->push('app.allow_content_scripts', 'true');
153
154         $script = 'abc123<script>console.log("hello-test")</script>abc123';
155         $page->html = "no escape {$script}";
156         $page->save();
157
158         $pageView = $this->get($page->getUrl());
159         $pageView->assertSee($script);
160         $pageView->assertDontSee('abc123abc123');
161     }
162
163     public function test_page_inline_on_attributes_show_if_configured()
164     {
165         $this->asEditor();
166         $page = Page::first();
167         config()->push('app.allow_content_scripts', 'true');
168
169         $script = '<p onmouseenter="console.log(\'test\')">Hello</p>';
170         $page->html = "escape {$script}";
171         $page->save();
172
173         $pageView = $this->get($page->getUrl());
174         $pageView->assertSee($script);
175         $pageView->assertDontSee('<p>Hello</p>');
176     }
177
178     public function test_duplicate_ids_does_not_break_page_render()
179     {
180         $this->asEditor();
181         $pageA = Page::first();
182         $pageB = Page::query()->where('id', '!=', $pageA->id)->first();
183
184         $content = '<ul id="bkmrk-xxx-%28"></ul> <ul id="bkmrk-xxx-%28"></ul>';
185         $pageA->html = $content;
186         $pageA->save();
187
188         $pageB->html = '<ul id="bkmrk-xxx-%28"></ul> <p>{{@'. $pageA->id .'#test}}</p>';
189         $pageB->save();
190
191         $pageView = $this->get($pageB->getUrl());
192         $pageView->assertSuccessful();
193     }
194
195     public function test_duplicate_ids_fixed_on_page_save()
196     {
197         $this->asEditor();
198         $page = Page::first();
199
200         $content = '<ul id="bkmrk-test"><li>test a</li><li><ul id="bkmrk-test"><li>test b</li></ul></li></ul>';
201         $pageSave = $this->put($page->getUrl(), [
202             'name' => $page->name,
203             'html' => $content,
204             'summary' => ''
205         ]);
206         $pageSave->assertRedirect();
207
208         $updatedPage = Page::where('id', '=', $page->id)->first();
209         $this->assertEquals(substr_count($updatedPage->html, "bkmrk-test\""), 1);
210     }
211 }