Skip to content

Commit cfcbeaf

Browse files
authored
All Readers - Allow or Forbid Fetching of External Images Release210 (#4546)
* All Readers - Allow or Forbid Fetching of External Images Release210 Add to all readers the option to allow or forbid fetching external images. This is unconditionally allowed now. The default will be set to "allow", so no code changes are necessary. However, we are giving consideration to changing the default. * Update Changelog
1 parent b211762 commit cfcbeaf

File tree

9 files changed

+133
-11
lines changed

9 files changed

+133
-11
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com)
66
and this project adheres to [Semantic Versioning](https://semver.org).
77

8+
# 2025-07-23 - 2.1.11
9+
10+
### Added
11+
12+
- Add to all readers the option to allow or forbid fetching external images. This is unconditionally allowed now. The default will be set to "allow", so no code changes are necessary. However, we are giving consideration to changing the default. [PR #4546](https://github.com/PHPOffice/PhpSpreadsheet/pull/4546)
13+
814
# 2025-06-22 - 2.1.10
915

1016
### Changed

src/PhpSpreadsheet/Reader/BaseReader.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ abstract class BaseReader implements IReader
3939
*/
4040
protected ?array $loadSheetsOnly = null;
4141

42+
/**
43+
* Allow external images. Use with caution.
44+
* Improper specification of these within a spreadsheet
45+
* can subject the caller to security exploits.
46+
*/
47+
protected bool $allowExternalImages = true;
48+
4249
/**
4350
* IReadFilter instance.
4451
*/
@@ -125,6 +132,23 @@ public function setReadFilter(IReadFilter $readFilter): self
125132
return $this;
126133
}
127134

135+
/**
136+
* Allow external images. Use with caution.
137+
* Improper specification of these within a spreadsheet
138+
* can subject the caller to security exploits.
139+
*/
140+
public function setAllowExternalImages(bool $allowExternalImages): self
141+
{
142+
$this->allowExternalImages = $allowExternalImages;
143+
144+
return $this;
145+
}
146+
147+
public function getAllowExternalImages(): bool
148+
{
149+
return $this->allowExternalImages;
150+
}
151+
128152
public function getSecurityScanner(): ?XmlScanner
129153
{
130154
return $this->securityScanner;
@@ -150,6 +174,12 @@ protected function processFlags(int $flags): void
150174
if (((bool) ($flags & self::SKIP_EMPTY_CELLS) || (bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
151175
$this->setReadEmptyCells(false);
152176
}
177+
if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
178+
$this->setAllowExternalImages(true);
179+
}
180+
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
181+
$this->setAllowExternalImages(false);
182+
}
153183
}
154184

155185
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet

src/PhpSpreadsheet/Reader/Html.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1031,7 +1031,7 @@ private function insertImage(Worksheet $sheet, string $column, int $row, array $
10311031
$name = $attributes['alt'] ?? null;
10321032

10331033
$drawing = new Drawing();
1034-
$drawing->setPath($src, false);
1034+
$drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
10351035
if ($drawing->getPath() === '') {
10361036
return;
10371037
}

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ interface IReader
1313
public const SKIP_EMPTY_CELLS = 4;
1414
public const IGNORE_EMPTY_CELLS = 4;
1515

16+
/**
17+
* Allow external images. Use with caution.
18+
* Improper specification of these within a spreadsheet
19+
* can subject the caller to security exploits.
20+
*/
21+
public const ALLOW_EXTERNAL_IMAGES = 16;
22+
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
23+
1624
public function __construct();
1725

1826
/**
@@ -110,6 +118,15 @@ public function getReadFilter(): IReadFilter;
110118
*/
111119
public function setReadFilter(IReadFilter $readFilter): self;
112120

121+
/**
122+
* Allow external images. Use with caution.
123+
* Improper specification of these within a spreadsheet
124+
* can subject the caller to security exploits.
125+
*/
126+
public function setAllowExternalImages(bool $allowExternalImages): self;
127+
128+
public function getAllowExternalImages(): bool;
129+
113130
/**
114131
* Loads PhpSpreadsheet from file.
115132
*
@@ -119,6 +136,8 @@ public function setReadFilter(IReadFilter $readFilter): self;
119136
* self::READ_DATA_ONLY Read only data, not style or structure information, from the file
120137
* self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
121138
* empty string, or a string containing only whitespace characters)
139+
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
140+
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
122141
*/
123142
public function load(string $filename, int $flags = 0): Spreadsheet;
124143
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,7 +1441,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14411441
);
14421442
if (isset($images[$linkImageKey])) {
14431443
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1444-
$objDrawing->setPath($url, false);
1444+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
14451445
}
14461446
if ($objDrawing->getPath() === '') {
14471447
continue;
@@ -1534,7 +1534,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
15341534
);
15351535
if (isset($images[$linkImageKey])) {
15361536
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1537-
$objDrawing->setPath($url, false);
1537+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
15381538
}
15391539
if ($objDrawing->getPath() === '') {
15401540
continue;

src/PhpSpreadsheet/Worksheet/Drawing.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public function getPath(): string
9292
*
9393
* @return $this
9494
*/
95-
public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null): static
95+
public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip = null, bool $allowExternal = true): static
9696
{
9797
$this->isUrl = false;
9898
if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) {
@@ -107,6 +107,9 @@ public function setPath(string $path, bool $verifyFile = true, ?ZipArchive $zip
107107
if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) {
108108
throw new PhpSpreadsheetException('Invalid protocol for linked drawing');
109109
}
110+
if (!$allowExternal) {
111+
return $this;
112+
}
110113
// Implicit that it is a URL, rather store info than running check above on value in other places.
111114
$this->isUrl = true;
112115
$ctx = null;

tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@ public static function createHtml(string $html): string
1818
return $filename;
1919
}
2020

21-
public static function loadHtmlIntoSpreadsheet(string $filename, bool $unlink = false): Spreadsheet
21+
public static function loadHtmlIntoSpreadsheet(string $filename, bool $unlink = false, ?bool $allowExternalImages = null): Spreadsheet
2222
{
2323
$html = new Html();
24+
if ($allowExternalImages !== null) {
25+
$html->setAllowExternalImages($allowExternalImages);
26+
}
2427
$spreadsheet = $html->load($filename);
2528
if ($unlink) {
2629
unlink($filename);

tests/PhpSpreadsheetTests/Reader/Html/HtmlImage2Test.php

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class HtmlImage2Test extends TestCase
1313
{
14-
public function testCanInsertImageGoodProtocol(): void
14+
public function testCanInsertImageGoodProtocolAllowed(): void
1515
{
1616
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
1717
self::markTestSkipped('Skipped due to setting of environment variable');
@@ -23,16 +23,32 @@ public function testCanInsertImageGoodProtocol(): void
2323
</tr>
2424
</table>';
2525
$filename = HtmlHelper::createHtml($html);
26-
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true);
26+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, true);
2727
$firstSheet = $spreadsheet->getSheet(0);
2828

2929
/** @var Drawing $drawing */
3030
$drawing = $firstSheet->getDrawingCollection()[0];
3131
self::assertEquals($imagePath, $drawing->getPath());
3232
self::assertEquals('A1', $drawing->getCoordinates());
33+
$spreadsheet->disconnectWorksheets();
3334
}
3435

35-
public function testCantInsertImageNotFound(): void
36+
public function testCanInsertImageGoodProtocolNotAllowed(): void
37+
{
38+
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/01-03-filter-icon-1.png';
39+
$html = '<table>
40+
<tr>
41+
<td><img src="' . $imagePath . '" alt="test image voilà"></td>
42+
</tr>
43+
</table>';
44+
$filename = HtmlHelper::createHtml($html);
45+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, false);
46+
$firstSheet = $spreadsheet->getSheet(0);
47+
self::assertCount(0, $firstSheet->getDrawingCollection());
48+
$spreadsheet->disconnectWorksheets();
49+
}
50+
51+
public function testCantInsertImageNotFoundAllowed(): void
3652
{
3753
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
3854
self::markTestSkipped('Skipped due to setting of environment variable');
@@ -44,10 +60,27 @@ public function testCantInsertImageNotFound(): void
4460
</tr>
4561
</table>';
4662
$filename = HtmlHelper::createHtml($html);
47-
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true);
63+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, true);
64+
$firstSheet = $spreadsheet->getSheet(0);
65+
$drawingCollection = $firstSheet->getDrawingCollection();
66+
self::assertCount(0, $drawingCollection);
67+
$spreadsheet->disconnectWorksheets();
68+
}
69+
70+
public function testCantInsertImageNotFoundNotAllowed(): void
71+
{
72+
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/xxx01-03-filter-icon-1.png';
73+
$html = '<table>
74+
<tr>
75+
<td><img src="' . $imagePath . '" alt="test image voilà"></td>
76+
</tr>
77+
</table>';
78+
$filename = HtmlHelper::createHtml($html);
79+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, false);
4880
$firstSheet = $spreadsheet->getSheet(0);
4981
$drawingCollection = $firstSheet->getDrawingCollection();
5082
self::assertCount(0, $drawingCollection);
83+
$spreadsheet->disconnectWorksheets();
5184
}
5285

5386
/**

tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212

1313
class URLImageTest extends TestCase
1414
{
15-
public function testURLImageSource(): void
15+
public function testURLImageSourceAllowed(): void
1616
{
1717
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
1818
self::markTestSkipped('Skipped due to setting of environment variable');
1919
}
2020
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx');
2121
self::assertNotFalse($filename);
2222
$reader = IOFactory::createReader('Xlsx');
23+
$reader->setAllowExternalImages(true);
2324
$spreadsheet = $reader->load($filename);
2425
$worksheet = $spreadsheet->getActiveSheet();
2526
$collection = $worksheet->getDrawingCollection();
@@ -37,14 +38,41 @@ public function testURLImageSource(): void
3738
}
3839
}
3940

40-
public function xtestURLImageSourceNotFound(): void
41+
public function testURLImageSourceNotAllowed(): void
42+
{
43+
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx');
44+
self::assertNotFalse($filename);
45+
$reader = IOFactory::createReader('Xlsx');
46+
$reader->setAllowExternalImages(false);
47+
$spreadsheet = $reader->load($filename);
48+
$worksheet = $spreadsheet->getActiveSheet();
49+
$collection = $worksheet->getDrawingCollection();
50+
self::assertCount(0, $collection);
51+
$spreadsheet->disconnectWorksheets();
52+
}
53+
54+
public function testURLImageSourceNotFoundAllowed(): void
4155
{
4256
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
4357
self::markTestSkipped('Skipped due to setting of environment variable');
4458
}
4559
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.notfound.xlsx');
4660
self::assertNotFalse($filename);
4761
$reader = IOFactory::createReader('Xlsx');
62+
$reader->setAllowExternalImages(true);
63+
$spreadsheet = $reader->load($filename);
64+
$worksheet = $spreadsheet->getActiveSheet();
65+
$collection = $worksheet->getDrawingCollection();
66+
self::assertCount(0, $collection);
67+
$spreadsheet->disconnectWorksheets();
68+
}
69+
70+
public function testURLImageSourceNotFoundNotAllowed(): void
71+
{
72+
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.notfound.xlsx');
73+
self::assertNotFalse($filename);
74+
$reader = IOFactory::createReader('Xlsx');
75+
$reader->setAllowExternalImages(false);
4876
$spreadsheet = $reader->load($filename);
4977
$worksheet = $spreadsheet->getActiveSheet();
5078
$collection = $worksheet->getDrawingCollection();

0 commit comments

Comments
 (0)