3 namespace Drupal\editor\Tests;
5 use Drupal\Component\Serialization\Json;
6 use Drupal\editor\Entity\Editor;
7 use Drupal\simpletest\WebTestBase;
8 use Drupal\filter\Entity\FilterFormat;
11 * Tests XSS protection for content creators when using text editors.
15 class EditorSecurityTest extends WebTestBase {
18 * The sample content to use in all tests.
22 protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
25 * The secured sample content to use in most tests.
29 protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
32 * The secured sample content to use in tests when the <embed> tag is allowed.
36 protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
43 public static $modules = ['filter', 'editor', 'editor_test', 'node'];
46 * User with access to Restricted HTML text format without text editor.
48 * @var \Drupal\user\UserInterface
50 protected $untrustedUser;
53 * User with access to Restricted HTML text format with text editor.
55 * @var \Drupal\user\UserInterface
57 protected $normalUser;
60 * User with access to Restricted HTML text format, dangerous tags allowed
63 * @var \Drupal\user\UserInterface
65 protected $trustedUser;
68 * User with access to all text formats and text editors.
70 * @var \Drupal\user\UserInterface
72 protected $privilegedUser;
74 protected function setUp() {
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',
91 // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
95 'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
101 $format = FilterFormat::create([
102 'format' => 'restricted_with_editor',
103 'name' => 'Restricted HTML, with text editor',
106 // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
110 'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
116 $editor = Editor::create([
117 'format' => 'restricted_with_editor',
118 'editor' => 'unicorn',
121 $format = FilterFormat::create([
122 'format' => 'restricted_plus_dangerous_tag_with_editor',
123 'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
126 // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
130 'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
136 $editor = Editor::create([
137 'format' => 'restricted_plus_dangerous_tag_with_editor',
138 'editor' => 'unicorn',
141 $format = FilterFormat::create([
142 'format' => 'unrestricted_without_editor',
143 'name' => 'Unrestricted HTML, without text editor',
148 $format = FilterFormat::create([
149 'format' => 'unrestricted_with_editor',
150 'name' => 'Unrestricted HTML, with text editor',
155 $editor = Editor::create([
156 'format' => 'unrestricted_with_editor',
157 'editor' => 'unicorn',
162 $this->drupalCreateContentType([
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',
179 $this->normalUser = $this->drupalCreateUser([
180 'create article content',
181 'edit any article content',
182 'use text format restricted_with_editor',
184 $this->trustedUser = $this->drupalCreateUser([
185 'create article content',
186 'edit any article content',
187 'use text format restricted_plus_dangerous_tag_with_editor',
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',
199 // Create an "article" node for each possible text format, with the same
200 // sample content, to do our tests on.
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'],
208 foreach ($samples as $sample) {
209 $this->drupalCreateNode([
212 ['value' => self::$sampleContent, 'format' => $sample['format']]
214 'uid' => $sample['author']
220 * Tests initial security: is the user safe without switching text formats?
222 * Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
224 public function testInitialSecurity() {
228 'format' => 'restricted_without_editor',
229 // No text editor => no XSS filtering.
230 'value' => self::$sampleContent,
232 $this->untrustedUser,
233 $this->privilegedUser,
238 'format' => 'restricted_with_editor',
239 // Text editor => XSS filtering.
240 'value' => self::$sampleContentSecured,
243 $this->privilegedUser,
248 'format' => 'restricted_plus_dangerous_tag_with_editor',
249 // Text editor => XSS filtering.
250 'value' => self::$sampleContentSecuredEmbedAllowed,
253 $this->privilegedUser,
258 'format' => 'unrestricted_without_editor',
259 // No text editor => no XSS filtering.
260 'value' => self::$sampleContent,
262 $this->privilegedUser,
267 'format' => 'unrestricted_with_editor',
268 // Text editor, no security filter => no XSS filtering.
269 'value' => self::$sampleContent,
271 $this->privilegedUser,
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'],
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'], (string) $dom_node[0], 'The value was correctly filtered for XSS attack vectors.');
292 * Tests administrator security: is the user safe when switching text formats?
294 * Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
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.
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.
304 public function testSwitchingSecurity() {
308 // No text editor => no XSS filtering.
309 'value' => self::$sampleContent,
310 'format' => 'restricted_without_editor',
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,
322 // Text editor => XSS filtering.
323 'value' => self::$sampleContentSecured,
324 'format' => 'restricted_with_editor',
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,
337 // Text editor => XSS filtering.
338 'value' => self::$sampleContentSecuredEmbedAllowed,
339 'format' => 'restricted_plus_dangerous_tag_with_editor',
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,
353 // No text editor => no XSS filtering.
354 'value' => self::$sampleContent,
355 'format' => 'unrestricted_without_editor',
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,
370 // Text editor => XSS filtering.
371 'value' => self::$sampleContentSecured,
372 'format' => 'unrestricted_with_editor',
374 // From editor, no security filters to security filters, no editor: no
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,
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 foreach ($expected as $case) {
393 $this->drupalGet('node/' . $case['node_id'] . '/edit');
395 // Verify data- attributes.
396 $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
397 $this->assertIdentical(self::$sampleContent, (string) $dom_node[0]['data-editor-value-original'], 'The data-editor-value-original attribute is correctly set.');
398 $this->assertIdentical('false', (string) $dom_node[0]['data-editor-value-is-changed'], 'The data-editor-value-is-changed attribute is correctly set.');
400 // Switch to every other text format/editor and verify the results.
401 foreach ($case['switch_to'] as $format => $expected_filtered_value) {
402 $this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', [
403 '%sample_id' => $case['node_id'],
404 '%original_format' => $case['format'],
405 '%format' => $format,
408 'value' => self::$sampleContent,
409 'original_format_id' => $case['format'],
411 $response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post);
412 $this->assertResponse(200);
413 $json = Json::decode($response);
414 $this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
420 * Tests the standard text editor XSS filter being overridden.
422 public function testEditorXssFilterOverride() {
423 // First: the Standard text editor XSS filter.
424 $this->drupalLogin($this->normalUser);
425 $this->drupalGet('node/2/edit');
426 $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
427 $this->assertIdentical(self::$sampleContentSecured, (string) $dom_node[0], 'The value was filtered by the Standard text editor XSS filter.');
429 // Enable editor_test.module's hook_editor_xss_filter_alter() implementation
430 // to alter the text editor XSS filter class being used.
431 \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
433 // First: the Insecure text editor XSS filter.
434 $this->drupalGet('node/2/edit');
435 $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
436 $this->assertIdentical(self::$sampleContent, (string) $dom_node[0], 'The value was filtered by the Insecure text editor XSS filter.');