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
diff --git a/web/core/modules/editor/tests/src/Functional/EditorSecurityTest.php b/web/core/modules/editor/tests/src/Functional/EditorSecurityTest.php
new file mode 100644 (file)
index 0000000..55095c9
--- /dev/null
@@ -0,0 +1,453 @@
+<?php
+
+namespace Drupal\Tests\editor\Functional;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests XSS protection for content creators when using text editors.
+ *
+ * @group editor
+ */
+class EditorSecurityTest extends BrowserTestBase {
+
+  /**
+   * The sample content to use in all tests.
+   *
+   * @var string
+   */
+  protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
+
+  /**
+   * The secured sample content to use in most tests.
+   *
+   * @var string
+   */
+  protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
+
+  /**
+   * The secured sample content to use in tests when the <embed> tag is allowed.
+   *
+   * @var string
+   */
+  protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['filter', 'editor', 'editor_test', 'node'];
+
+  /**
+   * User with access to Restricted HTML text format without text editor.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $untrustedUser;
+
+  /**
+   * User with access to Restricted HTML text format with text editor.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $normalUser;
+
+  /**
+   * User with access to Restricted HTML text format, dangerous tags allowed
+   * with text editor.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $trustedUser;
+
+  /**
+   * User with access to all text formats and text editors.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $privilegedUser;
+
+  protected function setUp() {
+    parent::setUp();
+
+    // Create 5 text formats, to cover all potential use cases:
+    //  1. restricted_without_editor (untrusted: anonymous)
+    //  2. restricted_with_editor (normal: authenticated)
+    //  3. restricted_plus_dangerous_tag_with_editor (privileged: trusted)
+    //  4. unrestricted_without_editor (privileged: admin)
+    //  5. unrestricted_with_editor (privileged: admin)
+    // With text formats 2, 3 and 5, we also associate a text editor that does
+    // not guarantee XSS safety. "restricted" means the text format has XSS
+    // filters on output, "unrestricted" means the opposite.
+    $format = FilterFormat::create([
+      'format' => 'restricted_without_editor',
+      'name' => 'Restricted HTML, without text editor',
+      'weight' => 0,
+      'filters' => [
+        // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
+          ],
+        ],
+      ],
+    ]);
+    $format->save();
+    $format = FilterFormat::create([
+      'format' => 'restricted_with_editor',
+      'name' => 'Restricted HTML, with text editor',
+      'weight' => 1,
+      'filters' => [
+        // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
+          ],
+        ],
+      ],
+    ]);
+    $format->save();
+    $editor = Editor::create([
+      'format' => 'restricted_with_editor',
+      'editor' => 'unicorn',
+    ]);
+    $editor->save();
+    $format = FilterFormat::create([
+      'format' => 'restricted_plus_dangerous_tag_with_editor',
+      'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
+      'weight' => 1,
+      'filters' => [
+        // A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
+        'filter_html' => [
+          'status' => 1,
+          'settings' => [
+            'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
+          ],
+        ],
+      ],
+    ]);
+    $format->save();
+    $editor = Editor::create([
+      'format' => 'restricted_plus_dangerous_tag_with_editor',
+      'editor' => 'unicorn',
+    ]);
+    $editor->save();
+    $format = FilterFormat::create([
+      'format' => 'unrestricted_without_editor',
+      'name' => 'Unrestricted HTML, without text editor',
+      'weight' => 0,
+      'filters' => [],
+    ]);
+    $format->save();
+    $format = FilterFormat::create([
+      'format' => 'unrestricted_with_editor',
+      'name' => 'Unrestricted HTML, with text editor',
+      'weight' => 1,
+      'filters' => [],
+    ]);
+    $format->save();
+    $editor = Editor::create([
+      'format' => 'unrestricted_with_editor',
+      'editor' => 'unicorn',
+    ]);
+    $editor->save();
+
+    // Create node type.
+    $this->drupalCreateContentType([
+      'type' => 'article',
+      'name' => 'Article',
+    ]);
+
+    // Create 4 users, each with access to different text formats/editors:
+    //   - "untrusted": restricted_without_editor
+    //   - "normal": restricted_with_editor,
+    //   - "trusted": restricted_plus_dangerous_tag_with_editor
+    //   - "privileged": restricted_without_editor, restricted_with_editor,
+    //     restricted_plus_dangerous_tag_with_editor,
+    //     unrestricted_without_editor and unrestricted_with_editor
+    $this->untrustedUser = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'use text format restricted_without_editor',
+    ]);
+    $this->normalUser = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'use text format restricted_with_editor',
+    ]);
+    $this->trustedUser = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'use text format restricted_plus_dangerous_tag_with_editor',
+    ]);
+    $this->privilegedUser = $this->drupalCreateUser([
+      'create article content',
+      'edit any article content',
+      'use text format restricted_without_editor',
+      'use text format restricted_with_editor',
+      'use text format restricted_plus_dangerous_tag_with_editor',
+      'use text format unrestricted_without_editor',
+      'use text format unrestricted_with_editor',
+    ]);
+
+    // Create an "article" node for each possible text format, with the same
+    // sample content, to do our tests on.
+    $samples = [
+      ['author' => $this->untrustedUser->id(), 'format' => 'restricted_without_editor'],
+      ['author' => $this->normalUser->id(), 'format' => 'restricted_with_editor'],
+      ['author' => $this->trustedUser->id(), 'format' => 'restricted_plus_dangerous_tag_with_editor'],
+      ['author' => $this->privilegedUser->id(), 'format' => 'unrestricted_without_editor'],
+      ['author' => $this->privilegedUser->id(), 'format' => 'unrestricted_with_editor'],
+    ];
+    foreach ($samples as $sample) {
+      $this->drupalCreateNode([
+        'type' => 'article',
+        'body' => [
+          ['value' => self::$sampleContent, 'format' => $sample['format']],
+        ],
+        'uid' => $sample['author'],
+      ]);
+    }
+  }
+
+  /**
+   * Tests initial security: is the user safe without switching text formats?
+   *
+   * Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
+   */
+  public function testInitialSecurity() {
+    $expected = [
+      [
+        'node_id' => 1,
+        'format' => 'restricted_without_editor',
+        // No text editor => no XSS filtering.
+        'value' => self::$sampleContent,
+        'users' => [
+          $this->untrustedUser,
+          $this->privilegedUser,
+        ],
+      ],
+      [
+        'node_id' => 2,
+        'format' => 'restricted_with_editor',
+        // Text editor => XSS filtering.
+        'value' => self::$sampleContentSecured,
+        'users' => [
+          $this->normalUser,
+          $this->privilegedUser,
+        ],
+      ],
+      [
+        'node_id' => 3,
+        'format' => 'restricted_plus_dangerous_tag_with_editor',
+        // Text editor => XSS filtering.
+        'value' => self::$sampleContentSecuredEmbedAllowed,
+        'users' => [
+          $this->trustedUser,
+          $this->privilegedUser,
+        ],
+      ],
+      [
+        'node_id' => 4,
+        'format' => 'unrestricted_without_editor',
+        // No text editor => no XSS filtering.
+        'value' => self::$sampleContent,
+        'users' => [
+          $this->privilegedUser,
+        ],
+      ],
+      [
+        'node_id' => 5,
+        'format' => 'unrestricted_with_editor',
+        // Text editor, no security filter => no XSS filtering.
+        'value' => self::$sampleContent,
+        'users' => [
+          $this->privilegedUser,
+        ],
+      ],
+    ];
+
+    // Log in as each user that may edit the content, and assert the value.
+    foreach ($expected as $case) {
+      foreach ($case['users'] as $account) {
+        $this->pass(format_string('Scenario: sample %sample_id, %format.', [
+          '%sample_id' => $case['node_id'],
+          '%format' => $case['format'],
+        ]));
+        $this->drupalLogin($account);
+        $this->drupalGet('node/' . $case['node_id'] . '/edit');
+        $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
+        $this->assertIdentical($case['value'], $dom_node[0]->getText(), 'The value was correctly filtered for XSS attack vectors.');
+      }
+    }
+  }
+
+  /**
+   * Tests administrator security: is the user safe when switching text formats?
+   *
+   * Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
+   *
+   * When changing from a more restrictive text format with a text editor (or a
+   * text format without a text editor) to a less restrictive text format, it is
+   * possible that a malicious user could trigger an XSS.
+   *
+   * E.g. when switching a piece of text that uses the Restricted HTML text
+   * format and contains a <script> tag to the Full HTML text format, the
+   * <script> tag would be executed. Unless we apply appropriate filtering.
+   */
+  public function testSwitchingSecurity() {
+    $expected = [
+      [
+        'node_id' => 1,
+        // No text editor => no XSS filtering.
+        'value' => self::$sampleContent,
+        'format' => 'restricted_without_editor',
+        'switch_to' => [
+          'restricted_with_editor' => self::$sampleContentSecured,
+          // Intersection of restrictions => most strict XSS filtering.
+          'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
+          // No text editor => no XSS filtering.
+          'unrestricted_without_editor' => FALSE,
+          'unrestricted_with_editor' => self::$sampleContentSecured,
+        ],
+      ],
+      [
+        'node_id' => 2,
+        // Text editor => XSS filtering.
+        'value' => self::$sampleContentSecured,
+        'format' => 'restricted_with_editor',
+        'switch_to' => [
+          // No text editor => no XSS filtering.
+          'restricted_without_editor' => FALSE,
+          // Intersection of restrictions => most strict XSS filtering.
+          'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
+          // No text editor => no XSS filtering.
+          'unrestricted_without_editor' => FALSE,
+          'unrestricted_with_editor' => self::$sampleContentSecured,
+        ],
+      ],
+      [
+        'node_id' => 3,
+        // Text editor => XSS filtering.
+        'value' => self::$sampleContentSecuredEmbedAllowed,
+        'format' => 'restricted_plus_dangerous_tag_with_editor',
+        'switch_to' => [
+          // No text editor => no XSS filtering.
+          'restricted_without_editor' => FALSE,
+          // Intersection of restrictions => most strict XSS filtering.
+          'restricted_with_editor' => self::$sampleContentSecured,
+          // No text editor => no XSS filtering.
+          'unrestricted_without_editor' => FALSE,
+          // Intersection of restrictions => most strict XSS filtering.
+          'unrestricted_with_editor' => self::$sampleContentSecured,
+        ],
+      ],
+      [
+        'node_id' => 4,
+        // No text editor => no XSS filtering.
+        'value' => self::$sampleContent,
+        'format' => 'unrestricted_without_editor',
+        'switch_to' => [
+          // No text editor => no XSS filtering.
+          'restricted_without_editor' => FALSE,
+          'restricted_with_editor' => self::$sampleContentSecured,
+          // Intersection of restrictions => most strict XSS filtering.
+          'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
+          // From no editor, no security filters, to editor, still no security
+          // filters: resulting content when viewed was already vulnerable, so
+          // it must be intentional.
+          'unrestricted_with_editor' => FALSE,
+        ],
+      ],
+      [
+        'node_id' => 5,
+        // Text editor => XSS filtering.
+        'value' => self::$sampleContentSecured,
+        'format' => 'unrestricted_with_editor',
+        'switch_to' => [
+          // From editor, no security filters to security filters, no editor: no
+          // risk.
+          'restricted_without_editor' => FALSE,
+          'restricted_with_editor' => self::$sampleContentSecured,
+          // Intersection of restrictions => most strict XSS filtering.
+          'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
+          // From no editor, no security filters, to editor, still no security
+          // filters: resulting content when viewed was already vulnerable, so
+          // it must be intentional.
+          'unrestricted_without_editor' => FALSE,
+        ],
+      ],
+    ];
+
+    // Log in as the privileged user, and for every sample, do the following:
+    //  - switch to every other text format/editor
+    //  - assert the XSS-filtered values that we get from the server
+    $this->drupalLogin($this->privilegedUser);
+    $cookies = $this->getSessionCookies();
+
+    foreach ($expected as $case) {
+      $this->drupalGet('node/' . $case['node_id'] . '/edit');
+
+      // Verify data- attributes.
+      $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
+      $this->assertIdentical(self::$sampleContent, $dom_node[0]->getAttribute('data-editor-value-original'), 'The data-editor-value-original attribute is correctly set.');
+      $this->assertIdentical('false', (string) $dom_node[0]->getAttribute('data-editor-value-is-changed'), 'The data-editor-value-is-changed attribute is correctly set.');
+
+      // Switch to every other text format/editor and verify the results.
+      foreach ($case['switch_to'] as $format => $expected_filtered_value) {
+        $this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', [
+          '%sample_id' => $case['node_id'],
+          '%original_format' => $case['format'],
+          '%format' => $format,
+        ]));
+
+        $post = [
+          'value' => self::$sampleContent,
+          'original_format_id' => $case['format'],
+        ];
+        $client = $this->getHttpClient();
+        $response = $client->post($this->buildUrl('/editor/filter_xss/' . $format), [
+          'body' => http_build_query($post),
+          'cookies' => $cookies,
+          'headers' => [
+            'Accept' => 'application/json',
+            'Content-Type' => 'application/x-www-form-urlencoded',
+          ],
+          'http_errors' => FALSE,
+        ]);
+
+        $this->assertEquals(200, $response->getStatusCode());
+
+        $json = Json::decode($response->getBody());
+        $this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
+      }
+    }
+  }
+
+  /**
+   * Tests the standard text editor XSS filter being overridden.
+   */
+  public function testEditorXssFilterOverride() {
+    // First: the Standard text editor XSS filter.
+    $this->drupalLogin($this->normalUser);
+    $this->drupalGet('node/2/edit');
+    $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
+    $this->assertIdentical(self::$sampleContentSecured, $dom_node[0]->getText(), 'The value was filtered by the Standard text editor XSS filter.');
+
+    // Enable editor_test.module's hook_editor_xss_filter_alter() implementation
+    // to alter the text editor XSS filter class being used.
+    \Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
+
+    // First: the Insecure text editor XSS filter.
+    $this->drupalGet('node/2/edit');
+    $dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
+    $this->assertIdentical(self::$sampleContent, $dom_node[0]->getText(), 'The value was filtered by the Insecure text editor XSS filter.');
+  }
+
+}