Skip to content

Commit 22058ce

Browse files
authored
All Readers - Allow or Forbid Fetching of External Images Release222 (#4547)
* All Readers - Allow or Forbid Fetching of External Images Release222 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 12e0d9f commit 22058ce

File tree

9 files changed

+132
-11
lines changed

9 files changed

+132
-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.3.10
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 #4547](https://github.com/PHPOffice/PhpSpreadsheet/pull/4547)
13+
814
# 2025-06-22 - 2.3.9
915

1016
### Changed

src/PhpSpreadsheet/Reader/BaseReader.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@ abstract class BaseReader implements IReader
4646
*/
4747
protected bool $ignoreRowsWithNoCells = false;
4848

49+
/**
50+
* Allow external images. Use with caution.
51+
* Improper specification of these within a spreadsheet
52+
* can subject the caller to security exploits.
53+
*/
54+
protected bool $allowExternalImages = true;
55+
4956
/**
5057
* IReadFilter instance.
5158
*/
@@ -144,6 +151,23 @@ public function setReadFilter(IReadFilter $readFilter): self
144151
return $this;
145152
}
146153

154+
/**
155+
* Allow external images. Use with caution.
156+
* Improper specification of these within a spreadsheet
157+
* can subject the caller to security exploits.
158+
*/
159+
public function setAllowExternalImages(bool $allowExternalImages): self
160+
{
161+
$this->allowExternalImages = $allowExternalImages;
162+
163+
return $this;
164+
}
165+
166+
public function getAllowExternalImages(): bool
167+
{
168+
return $this->allowExternalImages;
169+
}
170+
147171
public function getSecurityScanner(): ?XmlScanner
148172
{
149173
return $this->securityScanner;
@@ -172,6 +196,12 @@ protected function processFlags(int $flags): void
172196
if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {
173197
$this->setIgnoreRowsWithNoCells(true);
174198
}
199+
if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
200+
$this->setAllowExternalImages(true);
201+
}
202+
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
203+
$this->setAllowExternalImages(false);
204+
}
175205
}
176206

177207
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
@@ -1045,7 +1045,7 @@ private function insertImage(Worksheet $sheet, string $column, int $row, array $
10451045
$name = $attributes['alt'] ?? null;
10461046

10471047
$drawing = new Drawing();
1048-
$drawing->setPath($src, false);
1048+
$drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
10491049
if ($drawing->getPath() === '') {
10501050
return;
10511051
}

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ interface IReader
1515

1616
public const IGNORE_ROWS_WITH_NO_CELLS = 8;
1717

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

2028
/**
@@ -112,6 +120,15 @@ public function getReadFilter(): IReadFilter;
112120
*/
113121
public function setReadFilter(IReadFilter $readFilter): self;
114122

123+
/**
124+
* Allow external images. Use with caution.
125+
* Improper specification of these within a spreadsheet
126+
* can subject the caller to security exploits.
127+
*/
128+
public function setAllowExternalImages(bool $allowExternalImages): self;
129+
130+
public function getAllowExternalImages(): bool;
131+
115132
/**
116133
* Loads PhpSpreadsheet from file.
117134
*
@@ -121,6 +138,9 @@ public function setReadFilter(IReadFilter $readFilter): self;
121138
* self::READ_DATA_ONLY Read only data, not style or structure information, from the file
122139
* self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
123140
* empty string, or a string containing only whitespace characters)
141+
* self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells.
142+
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
143+
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
124144
*/
125145
public function load(string $filename, int $flags = 0): Spreadsheet;
126146
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1457,7 +1457,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14571457
);
14581458
if (isset($images[$linkImageKey])) {
14591459
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1460-
$objDrawing->setPath($url, false);
1460+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
14611461
}
14621462
if ($objDrawing->getPath() === '') {
14631463
continue;
@@ -1550,7 +1550,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
15501550
);
15511551
if (isset($images[$linkImageKey])) {
15521552
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1553-
$objDrawing->setPath($url, false);
1553+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
15541554
}
15551555
if ($objDrawing->getPath() === '') {
15561556
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: 35 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,7 +60,22 @@ 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+
}
68+
69+
public function testCantInsertImageNotFoundNotAllowed(): void
70+
{
71+
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/xxx01-03-filter-icon-1.png';
72+
$html = '<table>
73+
<tr>
74+
<td><img src="' . $imagePath . '" alt="test image voilà"></td>
75+
</tr>
76+
</table>';
77+
$filename = HtmlHelper::createHtml($html);
78+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, false);
4879
$firstSheet = $spreadsheet->getSheet(0);
4980
$drawingCollection = $firstSheet->getDrawingCollection();
5081
self::assertCount(0, $drawingCollection);

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
$spreadsheet->disconnectWorksheets();
3839
}
3940

40-
public function testURLImageSourceNotFound(): 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)