View source
<?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;
class EditorSecurityTest extends BrowserTestBase {
protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
protected $defaultTheme = 'stark';
protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
public static $modules = [
'filter',
'editor',
'editor_test',
'node',
];
protected $untrustedUser;
protected $normalUser;
protected $trustedUser;
protected $privilegedUser;
protected function setUp() {
parent::setUp();
$format = FilterFormat::create([
'format' => 'restricted_without_editor',
'name' => 'Restricted HTML, without text editor',
'weight' => 0,
'filters' => [
'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' => [
'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' => [
'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();
$this
->drupalCreateContentType([
'type' => 'article',
'name' => 'Article',
]);
$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',
]);
$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'],
]);
}
}
public function testInitialSecurity() {
$expected = [
[
'node_id' => 1,
'format' => 'restricted_without_editor',
'value' => self::$sampleContent,
'users' => [
$this->untrustedUser,
$this->privilegedUser,
],
],
[
'node_id' => 2,
'format' => 'restricted_with_editor',
'value' => self::$sampleContentSecured,
'users' => [
$this->normalUser,
$this->privilegedUser,
],
],
[
'node_id' => 3,
'format' => 'restricted_plus_dangerous_tag_with_editor',
'value' => self::$sampleContentSecuredEmbedAllowed,
'users' => [
$this->trustedUser,
$this->privilegedUser,
],
],
[
'node_id' => 4,
'format' => 'unrestricted_without_editor',
'value' => self::$sampleContent,
'users' => [
$this->privilegedUser,
],
],
[
'node_id' => 5,
'format' => 'unrestricted_with_editor',
'value' => self::$sampleContent,
'users' => [
$this->privilegedUser,
],
],
];
foreach ($expected as $case) {
foreach ($case['users'] as $account) {
$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.');
}
}
}
public function testSwitchingSecurity() {
$expected = [
[
'node_id' => 1,
'value' => self::$sampleContent,
'format' => 'restricted_without_editor',
'switch_to' => [
'restricted_with_editor' => self::$sampleContentSecured,
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
'unrestricted_without_editor' => FALSE,
'unrestricted_with_editor' => self::$sampleContentSecured,
],
],
[
'node_id' => 2,
'value' => self::$sampleContentSecured,
'format' => 'restricted_with_editor',
'switch_to' => [
'restricted_without_editor' => FALSE,
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
'unrestricted_without_editor' => FALSE,
'unrestricted_with_editor' => self::$sampleContentSecured,
],
],
[
'node_id' => 3,
'value' => self::$sampleContentSecuredEmbedAllowed,
'format' => 'restricted_plus_dangerous_tag_with_editor',
'switch_to' => [
'restricted_without_editor' => FALSE,
'restricted_with_editor' => self::$sampleContentSecured,
'unrestricted_without_editor' => FALSE,
'unrestricted_with_editor' => self::$sampleContentSecured,
],
],
[
'node_id' => 4,
'value' => self::$sampleContent,
'format' => 'unrestricted_without_editor',
'switch_to' => [
'restricted_without_editor' => FALSE,
'restricted_with_editor' => self::$sampleContentSecured,
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
'unrestricted_with_editor' => FALSE,
],
],
[
'node_id' => 5,
'value' => self::$sampleContentSecured,
'format' => 'unrestricted_with_editor',
'switch_to' => [
'restricted_without_editor' => FALSE,
'restricted_with_editor' => self::$sampleContentSecured,
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
'unrestricted_without_editor' => FALSE,
],
],
];
$this
->drupalLogin($this->privilegedUser);
$cookies = $this
->getSessionCookies();
foreach ($expected as $case) {
$this
->drupalGet('node/' . $case['node_id'] . '/edit');
$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.');
foreach ($case['switch_to'] as $format => $expected_filtered_value) {
$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.');
}
}
}
public function testEditorXssFilterOverride() {
$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.');
\Drupal::state()
->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
$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.');
}
}