]> BookStack Code Mirror - bookstack/blob - tests/Uploads/AttachmentTest.php
Prevented possible XSS via link attachments
[bookstack] / tests / Uploads / AttachmentTest.php
1 <?php namespace Tests\Uploads;
2
3 use BookStack\Uploads\Attachment;
4 use BookStack\Entities\Page;
5 use BookStack\Auth\Permissions\PermissionService;
6 use BookStack\Uploads\AttachmentService;
7 use Illuminate\Http\UploadedFile;
8 use Tests\TestCase;
9 use Tests\TestResponse;
10
11 class AttachmentTest extends TestCase
12 {
13     /**
14      * Get a test file that can be uploaded
15      */
16     protected function getTestFile(string $fileName): UploadedFile
17     {
18         return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', 55, null, true);
19     }
20
21     /**
22      * Uploads a file with the given name.
23      */
24     protected function uploadFile(string $name, int $uploadedTo = 0): \Illuminate\Foundation\Testing\TestResponse
25     {
26         $file = $this->getTestFile($name);
27         return $this->call('POST', '/attachments/upload', ['uploaded_to' => $uploadedTo], [], ['file' => $file], []);
28     }
29
30     /**
31      * Create a new attachment
32      */
33     protected function createAttachment(Page $page): Attachment
34     {
35         $this->post('attachments/link', [
36             'attachment_link_url' => 'https://example.com',
37             'attachment_link_name' => 'Example Attachment Link',
38             'attachment_link_uploaded_to' => $page->id,
39         ]);
40
41         return Attachment::query()->latest()->first();
42     }
43
44     /**
45      * Delete all uploaded files.
46      * To assist with cleanup.
47      */
48     protected function deleteUploads()
49     {
50         $fileService = $this->app->make(AttachmentService::class);
51         foreach (Attachment::all() as $file) {
52             $fileService->deleteFile($file);
53         }
54     }
55
56     public function test_file_upload()
57     {
58         $page = Page::first();
59         $this->asAdmin();
60         $admin = $this->getAdmin();
61         $fileName = 'upload_test_file.txt';
62
63         $expectedResp = [
64             'name' => $fileName,
65             'uploaded_to'=> $page->id,
66             'extension' => 'txt',
67             'order' => 1,
68             'created_by' => $admin->id,
69             'updated_by' => $admin->id,
70         ];
71
72         $upload = $this->uploadFile($fileName, $page->id);
73         $upload->assertStatus(200);
74
75         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
76         $expectedResp['path'] = $attachment->path;
77
78         $upload->assertJson($expectedResp);
79         $this->assertDatabaseHas('attachments', $expectedResp);
80
81         $this->deleteUploads();
82     }
83
84     public function test_file_upload_does_not_use_filename()
85     {
86         $page = Page::first();
87         $fileName = 'upload_test_file.txt';
88
89
90         $upload = $this->asAdmin()->uploadFile($fileName, $page->id);
91         $upload->assertStatus(200);
92
93         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
94         $this->assertStringNotContainsString($fileName, $attachment->path);
95         $this->assertStringEndsWith('.txt', $attachment->path);
96     }
97
98     public function test_file_display_and_access()
99     {
100         $page = Page::first();
101         $this->asAdmin();
102         $fileName = 'upload_test_file.txt';
103
104         $upload = $this->uploadFile($fileName, $page->id);
105         $upload->assertStatus(200);
106         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
107
108         $pageGet = $this->get($page->getUrl());
109         $pageGet->assertSeeText($fileName);
110         $pageGet->assertSee($attachment->getUrl());
111
112         $attachmentGet = $this->get($attachment->getUrl());
113         $attachmentGet->assertSee('Hi, This is a test file for testing the upload process.');
114
115         $this->deleteUploads();
116     }
117
118     public function test_attaching_link_to_page()
119     {
120         $page = Page::first();
121         $admin = $this->getAdmin();
122         $this->asAdmin();
123
124         $linkReq = $this->call('POST', 'attachments/link', [
125             'attachment_link_url' => 'https://example.com',
126             'attachment_link_name' => 'Example Attachment Link',
127             'attachment_link_uploaded_to' => $page->id,
128         ]);
129
130         $expectedData = [
131             'path' => 'https://example.com',
132             'name' => 'Example Attachment Link',
133             'uploaded_to' => $page->id,
134             'created_by' => $admin->id,
135             'updated_by' => $admin->id,
136             'external' => true,
137             'order' => 1,
138             'extension' => ''
139         ];
140
141         $linkReq->assertStatus(200);
142         $this->assertDatabaseHas('attachments', $expectedData);
143         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
144
145         $pageGet = $this->get($page->getUrl());
146         $pageGet->assertSeeText('Example Attachment Link');
147         $pageGet->assertSee($attachment->getUrl());
148
149         $attachmentGet = $this->get($attachment->getUrl());
150         $attachmentGet->assertRedirect('https://example.com');
151
152         $this->deleteUploads();
153     }
154
155     public function test_attachment_updating()
156     {
157         $page = Page::first();
158         $this->asAdmin();
159
160         $attachment = $this->createAttachment($page);
161         $update = $this->call('PUT', 'attachments/' . $attachment->id, [
162             'attachment_edit_name' => 'My new attachment name',
163             'attachment_edit_url' => 'https://test.example.com'
164         ]);
165
166         $expectedData = [
167             'id' => $attachment->id,
168             'path' => 'https://test.example.com',
169             'name' => 'My new attachment name',
170             'uploaded_to' => $page->id
171         ];
172
173         $update->assertStatus(200);
174         $this->assertDatabaseHas('attachments', $expectedData);
175
176         $this->deleteUploads();
177     }
178
179     public function test_file_deletion()
180     {
181         $page = Page::first();
182         $this->asAdmin();
183         $fileName = 'deletion_test.txt';
184         $this->uploadFile($fileName, $page->id);
185
186         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
187         $filePath = storage_path($attachment->path);
188         $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
189
190         $attachment = Attachment::first();
191         $this->delete($attachment->getUrl());
192
193         $this->assertDatabaseMissing('attachments', [
194             'name' => $fileName
195         ]);
196         $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
197
198         $this->deleteUploads();
199     }
200
201     public function test_attachment_deletion_on_page_deletion()
202     {
203         $page = Page::first();
204         $this->asAdmin();
205         $fileName = 'deletion_test.txt';
206         $this->uploadFile($fileName, $page->id);
207
208         $attachment = Attachment::query()->orderBy('id', 'desc')->first();
209         $filePath = storage_path($attachment->path);
210
211         $this->assertTrue(file_exists($filePath), 'File at path ' . $filePath . ' does not exist');
212         $this->assertDatabaseHas('attachments', [
213             'name' => $fileName
214         ]);
215
216         $this->call('DELETE', $page->getUrl());
217
218         $this->assertDatabaseMissing('attachments', [
219             'name' => $fileName
220         ]);
221         $this->assertFalse(file_exists($filePath), 'File at path ' . $filePath . ' was not deleted as expected');
222
223         $this->deleteUploads();
224     }
225
226     public function test_attachment_access_without_permission_shows_404()
227     {
228         $admin = $this->getAdmin();
229         $viewer = $this->getViewer();
230         $page = Page::first(); /** @var Page $page */
231
232         $this->actingAs($admin);
233         $fileName = 'permission_test.txt';
234         $this->uploadFile($fileName, $page->id);
235         $attachment = Attachment::orderBy('id', 'desc')->take(1)->first();
236
237         $page->restricted = true;
238         $page->permissions()->delete();
239         $page->save();
240         $page->rebuildPermissions();
241         $page->load('jointPermissions');
242
243         $this->actingAs($viewer);
244         $attachmentGet = $this->get($attachment->getUrl());
245         $attachmentGet->assertStatus(404);
246         $attachmentGet->assertSee("Attachment not found");
247
248         $this->deleteUploads();
249     }
250
251     public function test_data_and_js_links_cannot_be_attached_to_a_page()
252     {
253         $page = Page::first();
254         $this->asAdmin();
255
256         $badLinks = [
257             'javascript:alert("bunny")',
258             ' javascript:alert("bunny")',
259             'JavaScript:alert("bunny")',
260             "\t\n\t\nJavaScript:alert(\"bunny\")",
261             "data:text/html;<a></a>",
262             "Data:text/html;<a></a>",
263             "Data:text/html;<a></a>",
264         ];
265
266         foreach ($badLinks as $badLink) {
267             $linkReq = $this->post('attachments/link', [
268                 'attachment_link_url' => $badLink,
269                 'attachment_link_name' => 'Example Attachment Link',
270                 'attachment_link_uploaded_to' => $page->id,
271             ]);
272             $linkReq->assertStatus(422);
273             $this->assertDatabaseMissing('attachments', [
274                 'path' => $badLink,
275             ]);
276         }
277
278         $attachment = $this->createAttachment($page);
279
280         foreach ($badLinks as $badLink) {
281             $linkReq = $this->put('attachments/' . $attachment->id, [
282                 'attachment_edit_url' => $badLink,
283                 'attachment_edit_name' => 'Example Attachment Link',
284             ]);
285             $linkReq->assertStatus(422);
286             $this->assertDatabaseMissing('attachments', [
287                 'path' => $badLink,
288             ]);
289         }
290     }
291 }