EntryTest.php 4.0 KB

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