Verified Commit 0ee07af8 authored by Jess's avatar Jess
Browse files

Issue #3530149 by xjm, smustgrave, catch, benjifisher, bramdriesen, longwave,...

Issue #3530149 by xjm, smustgrave, catch, benjifisher, bramdriesen, longwave, larowlan, alexpott, mcdruid: Add tests for SA-CORE-2025-004: Link field attribute XSS
parent ed874aed
Loading
Loading
Loading
Loading
Loading
+132 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\link\Kernel;

use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;

/**
 * Tests the Field Formatter for the link field type.
 *
 * @group link
 */
class LinkFormatterTest extends EntityKernelTestBase {

  /**
   * Modules to enable.
   *
   * @var array
   */
  protected static $modules = ['link'];

  /**
   * The entity type used in this test.
   *
   * @var string
   */
  protected string $entityType = 'entity_test';

  /**
   * The bundle used in this test.
   *
   * @var string
   */
  protected string $bundle = 'entity_test';

  /**
   * The name of the field used in this test.
   *
   * @var string
   */
  protected string $fieldName = 'field_test';

  /**
   * The entity to be tested.
   *
   * @var \Drupal\Core\Entity\EntityInterface
   */
  protected $entity;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Use Stark theme for testing markup output.
    \Drupal::service('theme_installer')->install(['stark']);
    $this->config('system.theme')->set('default', 'stark')->save();
    $this->installEntitySchema('entity_test');
    // Grant the 'view test entity' permission.
    $this->installConfig(['user']);
    Role::load(RoleInterface::ANONYMOUS_ID)
      ->grantPermission('view test entity')
      ->save();

    FieldStorageConfig::create([
      'field_name' => $this->fieldName,
      'type' => 'link',
      'entity_type' => $this->entityType,
      'cardinality' => 1,
    ])->save();

    FieldConfig::create([
      'field_name' => $this->fieldName,
      'entity_type' => $this->entityType,
      'bundle' => $this->bundle,
      'label' => 'Field test',
    ])->save();
  }

  /**
   * Tests the link formatters.
   *
   * @param string $formatter
   *   The name of the link formatter to test.
   *
   * @dataProvider providerLinkFormatter
   */
  public function testLinkFormatter(string $formatter): void {
    $entity = $this->container->get('entity_type.manager')
      ->getStorage($this->entityType)
      ->create([
        'name' => $this->randomMachineName(),
        $this->fieldName => [
          'uri' => 'https://www.drupal.org/',
          'title' => 'Hello world',
          'options' => [
            'attributes' => [
              'class' => 'classy',
              'onmouseover' => 'alert(document.cookie)',
            ],
          ],
        ],
      ]);
    $entity->save();

    $build = $entity->get($this->fieldName)->view(['type' => $formatter]);

    $renderer = $this->container->get('renderer');
    $renderer->renderRoot($build[0]);

    $output = (string) $build[0]['#markup'];
    $this->assertStringContainsString('<a href="https://www.drupal.org/" class="classy">', $output);
    $this->assertStringNotContainsString('onmouseover=', $output);
  }

  /**
   * Data provider for ::testLinkFormatter.
   */
  public static function providerLinkFormatter(): array {
    return [
      'default formatter' => ['link'],
      'separate link text and URL' => ['link_separate'],
    ];
  }

}
+126 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace Drupal\Tests\link\Unit;

use Drupal\link\AttributeXss;
use Drupal\Tests\UnitTestCase;

/**
 * Tests AttributeXss.
 *
 * @group link
 * @covers \Drupal\link\AttributeXss
 */
final class AttributeXssTest extends UnitTestCase {

  /**
   * Covers ::sanitizeAttributes.
   *
   * @dataProvider providerSanitizeAttributes
   */
  public function testSanitizeAttributes(array $attributes, array $expected): void {
    self::assertSame($expected, AttributeXss::sanitizeAttributes($attributes));
  }

  /**
   * Data provider for ::testSanitizeAttributes.
   *
   * @return \Generator
   *   Test cases.
   */
  public static function providerSanitizeAttributes(): \Generator {
    yield 'safe' => [
      ['class' => ['foo', 'bar'], 'data-biscuit' => TRUE],
      ['class' => ['foo', 'bar'], 'data-biscuit' => TRUE],
    ];

    yield 'valueless' => [
      ['class' => ['foo', 'bar'], 'selected' => ''],
      ['class' => ['foo', 'bar'], 'selected' => ''],
    ];

    yield 'empty names' => [
      ['class' => ['foo', 'bar'], '' => 'live', '  ' => TRUE],
      ['class' => ['foo', 'bar']],
    ];

    yield 'only empty names' => [
      ['' => 'live', '  ' => TRUE],
      [],
    ];

    yield 'valueless, mangled with a space' => [
      ['class' => ['foo', 'bar'], 'selected href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
    ];

    yield 'valueless, mangled with a space, blocked' => [
      ['class' => ['foo', 'bar'], 'selected onclick href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
    ];

    yield 'with encoding' => [
      ['class' => ['foo', 'bar'], 'data-how-good' => "It's the bee's knees"],
      ['class' => ['foo', 'bar'], 'data-how-good' => "It's the bee's knees"],
    ];

    yield 'valueless, mangled with multiple spaces, blocked' => [
      ['class' => ['foo', 'bar'], 'selected  onclick href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
    ];

    yield 'valueless, mangled with multiple spaces, blocked, mangled first' => [
      ['selected  onclick href' => 'http://example.com', 'class' => ['foo', 'bar']],
      ['selected' => 'selected', 'href' => 'http://example.com', 'class' => ['foo', 'bar']],
    ];

    yield 'valueless but with value' => [
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
    ];

    yield 'valueless but with value, bad protocol' => [
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'javascript:alert()'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'],
    ];

    yield 'valueless, mangled with a space and bad protocol' => [
      ['class' => ['foo', 'bar'], 'selected href' => 'javascript:alert()'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'],
    ];

    yield 'valueless, mangled with a space and bad protocol, repeated' => [
      ['class' => ['foo', 'bar'], 'selected href' => 'javascript:alert()', 'href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'alert()'],
    ];

    yield 'with a space' => [
      ['class' => ['foo', 'bar'], 'href' => \urlencode('some file.pdf')],
      ['class' => ['foo', 'bar'], 'href' => 'some+file.pdf'],
    ];

    yield 'with an unencoded space' => [
      ['class' => ['foo', 'bar'], 'href' => 'some file.pdf'],
      ['class' => ['foo', 'bar'], 'href' => 'some file.pdf'],
    ];

    yield 'xss onclick' => [
      ['class' => ['foo', 'bar'], 'onclick' => 'alert("whoop");'],
      ['class' => ['foo', 'bar']],
    ];

    yield 'xss onclick, valueless, mangled with a space' => [
      ['class' => ['foo', 'bar'], 'selected onclick href' => 'http://example.com'],
      ['class' => ['foo', 'bar'], 'selected' => 'selected', 'href' => 'http://example.com'],
    ];

    yield 'xss protocol' => [
      ['class' => ['foo', 'bar'], 'src' => 'javascript:alert("whoop");'],
      ['class' => ['foo', 'bar'], 'src' => 'alert("whoop");'],
    ];

  }

}
+29 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

namespace Drupal\Tests\menu_link_content\Kernel;

use Drupal\Core\Link;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\entity_test\Entity\EntityTestExternal;
use Drupal\KernelTests\KernelTestBase;
@@ -477,4 +478,32 @@ public function testMenuLinkContentFormInvalidParentMenu(): void {
    static::assertIsArray($build);
  }

  /**
   * Assert that attributes are filtered.
   */
  public function testXssFiltering(): void {
    $options = [
      'menu_name' => 'menu-test',
      'bundle' => 'menu_link_content',
      'link' => [
        [
          'uri' => 'https://www.drupal.org/',
          'options' => [
            'attributes' => [
              'class' => 'classy',
              'onmouseover' => 'alert(document.cookie)',
            ],
          ],
        ],
      ],
      'title' => 'Link test',
    ];
    $link = MenuLinkContent::create($options);
    $link->save();
    assert($link instanceof MenuLinkContent);
    $output = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toString()->getGeneratedLink();
    $this->assertStringContainsString('<a href="https://www.drupal.org/" class="classy">', $output);
    $this->assertStringNotContainsString('onmouseover=', $output);
  }

}