Updated Drupal to 8.6. This goes with the following updates because it's possible...
[yaffs-website] / web / core / modules / editor / tests / src / Functional / EditorSecurityTest.php
1 <?php
2
3 namespace Drupal\Tests\editor\Functional;
4
5 use Drupal\Component\Serialization\Json;
6 use Drupal\editor\Entity\Editor;
7 use Drupal\filter\Entity\FilterFormat;
8 use Drupal\Tests\BrowserTestBase;
9
10 /**
11  * Tests XSS protection for content creators when using text editors.
12  *
13  * @group editor
14  */
15 class EditorSecurityTest extends BrowserTestBase {
16
17   /**
18    * The sample content to use in all tests.
19    *
20    * @var string
21    */
22   protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
23
24   /**
25    * The secured sample content to use in most tests.
26    *
27    * @var string
28    */
29   protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
30
31   /**
32    * The secured sample content to use in tests when the <embed> tag is allowed.
33    *
34    * @var string
35    */
36   protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
37
38   /**
39    * Modules to enable.
40    *
41    * @var array
42    */
43   public static $modules = ['filter', 'editor', 'editor_test', 'node'];
44
45   /**
46    * User with access to Restricted HTML text format without text editor.
47    *
48    * @var \Drupal\user\UserInterface
49    */
50   protected $untrustedUser;
51
52   /**
53    * User with access to Restricted HTML text format with text editor.
54    *
55    * @var \Drupal\user\UserInterface
56    */
57   protected $normalUser;
58
59   /**
60    * User with access to Restricted HTML text format, dangerous tags allowed
61    * with text editor.
62    *
63    * @var \Drupal\user\UserInterface
64    */
65   protected $trustedUser;
66
67   /**
68    * User with access to all text formats and text editors.
69    *
70    * @var \Drupal\user\UserInterface
71    */
72   protected $privilegedUser;
73
74   protected function setUp() {
75     parent::setUp();
76
77     // Create 5 text formats, to cover all potential use cases:
78     //  1. restricted_without_editor (untrusted: anonymous)
79     //  2. restricted_with_editor (normal: authenticated)
80     //  3. restricted_plus_dangerous_tag_with_editor (privileged: trusted)
81     //  4. unrestricted_without_editor (privileged: admin)
82     //  5. unrestricted_with_editor (privileged: admin)
83     // With text formats 2, 3 and 5, we also associate a text editor that does
84     // not guarantee XSS safety. "restricted" means the text format has XSS
85     // filters on output, "unrestricted" means the opposite.
86     $format = FilterFormat::create([
87       'format' => 'restricted_without_editor',
88       'name' => 'Restricted HTML, without text editor',
89       'weight' => 0,
90       'filters' => [
91         // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
92         'filter_html' => [
93           'status' => 1,
94           'settings' => [
95             'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
96           ],
97         ],
98       ],
99     ]);
100     $format->save();
101     $format = FilterFormat::create([
102       'format' => 'restricted_with_editor',
103       'name' => 'Restricted HTML, with text editor',
104       'weight' => 1,
105       'filters' => [
106         // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
107         'filter_html' => [
108           'status' => 1,
109           'settings' => [
110             'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
111           ],
112         ],
113       ],
114     ]);
115     $format->save();
116     $editor = Editor::create([
117       'format' => 'restricted_with_editor',
118       'editor' => 'unicorn',
119     ]);
120     $editor->save();
121     $format = FilterFormat::create([
122       'format' => 'restricted_plus_dangerous_tag_with_editor',
123       'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
124       'weight' => 1,
125       'filters' => [
126         // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
127         'filter_html' => [
128           'status' => 1,
129           'settings' => [
130             'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
131           ],
132         ],
133       ],
134     ]);
135     $format->save();
136     $editor = Editor::create([
137       'format' => 'restricted_plus_dangerous_tag_with_editor',
138       'editor' => 'unicorn',
139     ]);
140     $editor->save();
141     $format = FilterFormat::create([
142       'format' => 'unrestricted_without_editor',
143       'name' => 'Unrestricted HTML, without text editor',
144       'weight' => 0,
145       'filters' => [],
146     ]);
147     $format->save();
148     $format = FilterFormat::create([
149       'format' => 'unrestricted_with_editor',
150       'name' => 'Unrestricted HTML, with text editor',
151       'weight' => 1,
152       'filters' => [],
153     ]);
154     $format->save();
155     $editor = Editor::create([
156       'format' => 'unrestricted_with_editor',
157       'editor' => 'unicorn',
158     ]);
159     $editor->save();
160
161     // Create node type.
162     $this->drupalCreateContentType([
163       'type' => 'article',
164       'name' => 'Article',
165     ]);
166
167     // Create 4 users, each with access to different text formats/editors:
168     //   - "untrusted": restricted_without_editor
169     //   - "normal": restricted_with_editor,
170     //   - "trusted": restricted_plus_dangerous_tag_with_editor
171     //   - "privileged": restricted_without_editor, restricted_with_editor,
172     //     restricted_plus_dangerous_tag_with_editor,
173     //     unrestricted_without_editor and unrestricted_with_editor
174     $this->untrustedUser = $this->drupalCreateUser([
175       'create article content',
176       'edit any article content',
177       'use text format restricted_without_editor',
178     ]);
179     $this->normalUser = $this->drupalCreateUser([
180       'create article content',
181       'edit any article content',
182       'use text format restricted_with_editor',
183     ]);
184     $this->trustedUser = $this->drupalCreateUser([
185       'create article content',
186       'edit any article content',
187       'use text format restricted_plus_dangerous_tag_with_editor',
188     ]);
189     $this->privilegedUser = $this->drupalCreateUser([
190       'create article content',
191       'edit any article content',
192       'use text format restricted_without_editor',
193       'use text format restricted_with_editor',
194       'use text format restricted_plus_dangerous_tag_with_editor',
195       'use text format unrestricted_without_editor',
196       'use text format unrestricted_with_editor',
197     ]);
198
199     // Create an "article" node for each possible text format, with the same
200     // sample content, to do our tests on.
201     $samples = [
202       ['author' => $this->untrustedUser->id(), 'format' => 'restricted_without_editor'],
203       ['author' => $this->normalUser->id(), 'format' => 'restricted_with_editor'],
204       ['author' => $this->trustedUser->id(), 'format' => 'restricted_plus_dangerous_tag_with_editor'],
205       ['author' => $this->privilegedUser->id(), 'format' => 'unrestricted_without_editor'],
206       ['author' => $this->privilegedUser->id(), 'format' => 'unrestricted_with_editor'],
207     ];
208     foreach ($samples as $sample) {
209       $this->drupalCreateNode([
210         'type' => 'article',
211         'body' => [
212           ['value' => self::$sampleContent, 'format' => $sample['format']],
213         ],
214         'uid' => $sample['author'],
215       ]);
216     }
217   }
218
219   /**
220    * Tests initial security: is the user safe without switching text formats?
221    *
222    * Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
223    */
224   public function testInitialSecurity() {
225     $expected = [
226       [
227         'node_id' => 1,
228         'format' => 'restricted_without_editor',
229         // No text editor => no XSS filtering.
230         'value' => self::$sampleContent,
231         'users' => [
232           $this->untrustedUser,
233           $this->privilegedUser,
234         ],
235       ],
236       [
237         'node_id' => 2,
238         'format' => 'restricted_with_editor',
239         // Text editor => XSS filtering.
240         'value' => self::$sampleContentSecured,
241         'users' => [
242           $this->normalUser,
243           $this->privilegedUser,
244         ],
245       ],
246       [
247         'node_id' => 3,
248         'format' => 'restricted_plus_dangerous_tag_with_editor',
249         // Text editor => XSS filtering.
250         'value' => self::$sampleContentSecuredEmbedAllowed,
251         'users' => [
252           $this->trustedUser,
253           $this->privilegedUser,
254         ],
255       ],
256       [
257         'node_id' => 4,
258         'format' => 'unrestricted_without_editor',
259         // No text editor => no XSS filtering.
260         'value' => self::$sampleContent,
261         'users' => [
262           $this->privilegedUser,
263         ],
264       ],
265       [
266         'node_id' => 5,
267         'format' => 'unrestricted_with_editor',
268         // Text editor, no security filter => no XSS filtering.
269         'value' => self::$sampleContent,
270         'users' => [
271           $this->privilegedUser,
272         ],
273       ],
274     ];
275
276     // Log in as each user that may edit the content, and assert the value.
277     foreach ($expected as $case) {
278       foreach ($case['users'] as $account) {
279         $this->pass(format_string('Scenario: sample %sample_id, %format.', [
280           '%sample_id' => $case['node_id'],
281           '%format' => $case['format'],
282         ]));
283         $this->drupalLogin($account);
284         $this->drupalGet('node/' . $case['node_id'] . '/edit');
285         $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
286         $this->assertIdentical($case['value'], $dom_node[0]->getText(), 'The value was correctly filtered for XSS attack vectors.');
287       }
288     }
289   }
290
291   /**
292    * Tests administrator security: is the user safe when switching text formats?
293    *
294    * Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
295    *
296    * When changing from a more restrictive text format with a text editor (or a
297    * text format without a text editor) to a less restrictive text format, it is
298    * possible that a malicious user could trigger an XSS.
299    *
300    * E.g. when switching a piece of text that uses the Restricted HTML text
301    * format and contains a <script> tag to the Full HTML text format, the
302    * <script> tag would be executed. Unless we apply appropriate filtering.
303    */
304   public function testSwitchingSecurity() {
305     $expected = [
306       [
307         'node_id' => 1,
308         // No text editor => no XSS filtering.
309         'value' => self::$sampleContent,
310         'format' => 'restricted_without_editor',
311         'switch_to' => [
312           'restricted_with_editor' => self::$sampleContentSecured,
313           // Intersection of restrictions => most strict XSS filtering.
314           'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
315           // No text editor => no XSS filtering.
316           'unrestricted_without_editor' => FALSE,
317           'unrestricted_with_editor' => self::$sampleContentSecured,
318         ],
319       ],
320       [
321         'node_id' => 2,
322         // Text editor => XSS filtering.
323         'value' => self::$sampleContentSecured,
324         'format' => 'restricted_with_editor',
325         'switch_to' => [
326           // No text editor => no XSS filtering.
327           'restricted_without_editor' => FALSE,
328           // Intersection of restrictions => most strict XSS filtering.
329           'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
330           // No text editor => no XSS filtering.
331           'unrestricted_without_editor' => FALSE,
332           'unrestricted_with_editor' => self::$sampleContentSecured,
333         ],
334       ],
335       [
336         'node_id' => 3,
337         // Text editor => XSS filtering.
338         'value' => self::$sampleContentSecuredEmbedAllowed,
339         'format' => 'restricted_plus_dangerous_tag_with_editor',
340         'switch_to' => [
341           // No text editor => no XSS filtering.
342           'restricted_without_editor' => FALSE,
343           // Intersection of restrictions => most strict XSS filtering.
344           'restricted_with_editor' => self::$sampleContentSecured,
345           // No text editor => no XSS filtering.
346           'unrestricted_without_editor' => FALSE,
347           // Intersection of restrictions => most strict XSS filtering.
348           'unrestricted_with_editor' => self::$sampleContentSecured,
349         ],
350       ],
351       [
352         'node_id' => 4,
353         // No text editor => no XSS filtering.
354         'value' => self::$sampleContent,
355         'format' => 'unrestricted_without_editor',
356         'switch_to' => [
357           // No text editor => no XSS filtering.
358           'restricted_without_editor' => FALSE,
359           'restricted_with_editor' => self::$sampleContentSecured,
360           // Intersection of restrictions => most strict XSS filtering.
361           'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
362           // From no editor, no security filters, to editor, still no security
363           // filters: resulting content when viewed was already vulnerable, so
364           // it must be intentional.
365           'unrestricted_with_editor' => FALSE,
366         ],
367       ],
368       [
369         'node_id' => 5,
370         // Text editor => XSS filtering.
371         'value' => self::$sampleContentSecured,
372         'format' => 'unrestricted_with_editor',
373         'switch_to' => [
374           // From editor, no security filters to security filters, no editor: no
375           // risk.
376           'restricted_without_editor' => FALSE,
377           'restricted_with_editor' => self::$sampleContentSecured,
378           // Intersection of restrictions => most strict XSS filtering.
379           'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
380           // From no editor, no security filters, to editor, still no security
381           // filters: resulting content when viewed was already vulnerable, so
382           // it must be intentional.
383           'unrestricted_without_editor' => FALSE,
384         ],
385       ],
386     ];
387
388     // Log in as the privileged user, and for every sample, do the following:
389     //  - switch to every other text format/editor
390     //  - assert the XSS-filtered values that we get from the server
391     $this->drupalLogin($this->privilegedUser);
392     $cookies = $this->getSessionCookies();
393
394     foreach ($expected as $case) {
395       $this->drupalGet('node/' . $case['node_id'] . '/edit');
396
397       // Verify data- attributes.
398       $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
399       $this->assertIdentical(self::$sampleContent, $dom_node[0]->getAttribute('data-editor-value-original'), 'The data-editor-value-original attribute is correctly set.');
400       $this->assertIdentical('false', (string) $dom_node[0]->getAttribute('data-editor-value-is-changed'), 'The data-editor-value-is-changed attribute is correctly set.');
401
402       // Switch to every other text format/editor and verify the results.
403       foreach ($case['switch_to'] as $format => $expected_filtered_value) {
404         $this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', [
405           '%sample_id' => $case['node_id'],
406           '%original_format' => $case['format'],
407           '%format' => $format,
408         ]));
409
410         $post = [
411           'value' => self::$sampleContent,
412           'original_format_id' => $case['format'],
413         ];
414         $client = $this->getHttpClient();
415         $response = $client->post($this->buildUrl('/editor/filter_xss/' . $format), [
416           'body' => http_build_query($post),
417           'cookies' => $cookies,
418           'headers' => [
419             'Accept' => 'application/json',
420             'Content-Type' => 'application/x-www-form-urlencoded',
421           ],
422           'http_errors' => FALSE,
423         ]);
424
425         $this->assertEquals(200, $response->getStatusCode());
426
427         $json = Json::decode($response->getBody());
428         $this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
429       }
430     }
431   }
432
433   /**
434    * Tests the standard text editor XSS filter being overridden.
435    */
436   public function testEditorXssFilterOverride() {
437     // First: the Standard text editor XSS filter.
438     $this->drupalLogin($this->normalUser);
439     $this->drupalGet('node/2/edit');
440     $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
441     $this->assertIdentical(self::$sampleContentSecured, $dom_node[0]->getText(), 'The value was filtered by the Standard text editor XSS filter.');
442
443     // Enable editor_test.module's hook_editor_xss_filter_alter() implementation
444     // to alter the text editor XSS filter class being used.
445     \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
446
447     // First: the Insecure text editor XSS filter.
448     $this->drupalGet('node/2/edit');
449     $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
450     $this->assertIdentical(self::$sampleContent, $dom_node[0]->getText(), 'The value was filtered by the Insecure text editor XSS filter.');
451   }
452
453 }