EntryTest.php 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. <?php
  2. declare(strict_types=1);
  3. final class EntryTest extends \PHPUnit\Framework\TestCase {
  4. #[\Override]
  5. public static function setUpBeforeClass(): void {
  6. FreshRSS_Context::initSystem();
  7. }
  8. /**
  9. * Parse a raw RSS payload through the real feed processing pipeline.
  10. * @return list<FreshRSS_Entry>
  11. */
  12. private static function entriesFromRss(string $rss): array {
  13. $feed = new FreshRSS_Feed('http://example.net/feed.xml', validate: false);
  14. $feed->_id(1);
  15. $simplePie = new FreshRSS_SimplePieCustom();
  16. $simplePie->enable_cache(false);
  17. $simplePie->set_raw_data($rss);
  18. self::assertTrue($simplePie->init());
  19. return array_values(iterator_to_array($feed->loadEntries($simplePie)));
  20. }
  21. public function test_content_dropsUnsafeEnclosureAndThumbnailUrls(): void {
  22. $rss = <<<XML
  23. <?xml version="1.0" encoding="UTF-8"?>
  24. <rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
  25. <channel>
  26. <title>Malicious feed</title>
  27. <link>https://example.net/</link>
  28. <description>Feed with malicious enclosure URLs</description>
  29. <item>
  30. <title>Victim Article</title>
  31. <link>https://example.net/article</link>
  32. <guid isPermaLink="false">poc-001</guid>
  33. <pubDate>Tue, 14 Nov 2023 20:13:20 +0000</pubDate>
  34. <description>Hello</description>
  35. <enclosure url="javascript:alert(document.domain)//" length="0" type="application/octet-stream" />
  36. <enclosure url="https://example.com/podcast.mp3" length="1234" type="audio/mpeg" />
  37. <enclosure url="//cdn.example.com/podcast2.mp3" length="1234" type="audio/mpeg" />
  38. <media:content url="https://example.com/pic.jpg" type="image/jpeg">
  39. <media:thumbnail url="javascript:alert(1)" />
  40. <media:thumbnail url="https://example.com/thumb.jpg" />
  41. <media:thumbnail url="//cdn.example.com/thumb2.jpg" />
  42. </media:content>
  43. <media:thumbnail url="javascript:alert(2)" />
  44. </item>
  45. </channel>
  46. </rss>
  47. XML;
  48. $entries = self::entriesFromRss($rss);
  49. self::assertCount(1, $entries);
  50. $entry = $entries[0];
  51. self::assertSame('Victim Article', $entry->title());
  52. // The malicious `<media:thumbnail>` of the item must not be stored as an attribute
  53. self::assertNull($entry->attributeArray('thumbnail'));
  54. // The malicious enclosure must not even be stored as an attribute
  55. $enclosureUrls = array_column($entry->attributeArray('enclosures') ?? [], 'url');
  56. self::assertNotContains('javascript:alert(document.domain)//', $enclosureUrls);
  57. self::assertContains('https://example.com/podcast.mp3', $enclosureUrls);
  58. // SimplePie must absolutise protocol-relative URLs against the feed URL
  59. self::assertContains('https://cdn.example.com/podcast2.mp3', $enclosureUrls);
  60. $html = $entry->content();
  61. self::assertStringNotContainsString('javascript:', $html);
  62. self::assertStringContainsString('href="https://example.com/podcast.mp3"', $html);
  63. self::assertStringContainsString('src="https://example.com/thumb.jpg"', $html);
  64. self::assertStringContainsString('href="https://cdn.example.com/podcast2.mp3"', $html);
  65. self::assertStringContainsString('src="https://cdn.example.com/thumb2.jpg"', $html);
  66. self::assertStringContainsString('Hello', $html);
  67. }
  68. public function test_content_dropsUnsafeUrlsFromLegacyAttributes(): void {
  69. $entry = new FreshRSS_Entry(1, 'poc-003', 'Victim Article', '', 'Hello', 'https://example.net/article');
  70. $entry->_attributes([
  71. 'thumbnail' => ['url' => 'javascript:alert(1)'],
  72. 'enclosures' => [
  73. ['url' => 'javascript:alert(2)', 'title' => 'evil'],
  74. ['url' => 'https://example.com/podcast.mp3', 'thumbnails' => ['javascript:alert(3)', 'https://example.com/thumb.jpg']],
  75. ],
  76. ]);
  77. $html = $entry->content();
  78. self::assertStringNotContainsString('javascript:', $html);
  79. self::assertStringContainsString('href="https://example.com/podcast.mp3"', $html);
  80. self::assertStringContainsString('src="https://example.com/thumb.jpg"', $html);
  81. self::assertStringContainsString('Hello', $html);
  82. }
  83. }