Skip to content

Commit 668d3b5

Browse files
authored
All Readers - Allow or Forbid Fetching of External Images Release390 (#4548)
* All Readers - Allow or Forbid Fetching of External Images Release390 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 3b07468 commit 668d3b5

File tree

9 files changed

+136
-11
lines changed

9 files changed

+136
-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 - 3.9.3
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 #4548](https://github.com/PHPOffice/PhpSpreadsheet/pull/4548)
13+
814
# 2025-06-22 - 3.9.2
915

1016
### Changed

src/PhpSpreadsheet/Reader/BaseReader.php

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

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

157+
/**
158+
* Allow external images. Use with caution.
159+
* Improper specification of these within a spreadsheet
160+
* can subject the caller to security exploits.
161+
*/
162+
public function setAllowExternalImages(bool $allowExternalImages): self
163+
{
164+
$this->allowExternalImages = $allowExternalImages;
165+
166+
return $this;
167+
}
168+
169+
public function getAllowExternalImages(): bool
170+
{
171+
return $this->allowExternalImages;
172+
}
173+
150174
public function getSecurityScanner(): ?XmlScanner
151175
{
152176
return $this->securityScanner;
@@ -175,6 +199,12 @@ protected function processFlags(int $flags): void
175199
if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {
176200
$this->setIgnoreRowsWithNoCells(true);
177201
}
202+
if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
203+
$this->setAllowExternalImages(true);
204+
}
205+
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
206+
$this->setAllowExternalImages(false);
207+
}
178208
}
179209

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

10921092
$drawing = new Drawing();
1093-
$drawing->setPath($src, false);
1093+
$drawing->setPath($src, false, allowExternal: $this->allowExternalImages);
10941094
if ($drawing->getPath() === '') {
10951095
return;
10961096
}

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ interface IReader
3838
*/
3939
public const IGNORE_ROWS_WITH_NO_CELLS = 8;
4040

41+
/**
42+
* Allow external images. Use with caution.
43+
* Improper specification of these within a spreadsheet
44+
* can subject the caller to security exploits.
45+
*/
46+
public const ALLOW_EXTERNAL_IMAGES = 16;
47+
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
48+
4149
public function __construct();
4250

4351
/**
@@ -128,6 +136,15 @@ public function setLoadAllSheets(): self;
128136
*/
129137
public function getReadFilter(): IReadFilter;
130138

139+
/**
140+
* Allow external images. Use with caution.
141+
* Improper specification of these within a spreadsheet
142+
* can subject the caller to security exploits.
143+
*/
144+
public function setAllowExternalImages(bool $allowExternalImages): self;
145+
146+
public function getAllowExternalImages(): bool;
147+
131148
/**
132149
* Set read filter.
133150
*
@@ -144,6 +161,9 @@ public function setReadFilter(IReadFilter $readFilter): self;
144161
* self::READ_DATA_ONLY Read only data, not style or structure information, from the file
145162
* self::IGNORE_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
146163
* empty string, or a string containing only whitespace characters)
164+
* self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells.
165+
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
166+
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
147167
*/
148168
public function load(string $filename, int $flags = 0): Spreadsheet;
149169
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1481,7 +1481,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14811481
);
14821482
if (isset($images[$linkImageKey])) {
14831483
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1484-
$objDrawing->setPath($url, false);
1484+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
14851485
}
14861486
if ($objDrawing->getPath() === '') {
14871487
continue;
@@ -1579,7 +1579,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
15791579
);
15801580
if (isset($images[$linkImageKey])) {
15811581
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1582-
$objDrawing->setPath($url, false);
1582+
$objDrawing->setPath($url, false, allowExternal: $this->allowExternalImages);
15831583
}
15841584
if ($objDrawing->getPath() === '') {
15851585
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: 39 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,34 @@ 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+
48+
$drawings = $firstSheet->getDrawingCollection();
49+
self::assertCount(0, $drawings);
50+
$spreadsheet->disconnectWorksheets();
51+
}
52+
53+
public function testCantInsertImageNotFoundAllowed(): void
3654
{
3755
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
3856
self::markTestSkipped('Skipped due to setting of environment variable');
@@ -44,10 +62,27 @@ public function testCantInsertImageNotFound(): void
4462
</tr>
4563
</table>';
4664
$filename = HtmlHelper::createHtml($html);
47-
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true);
65+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, true);
66+
$firstSheet = $spreadsheet->getSheet(0);
67+
$drawingCollection = $firstSheet->getDrawingCollection();
68+
self::assertCount(0, $drawingCollection);
69+
$spreadsheet->disconnectWorksheets();
70+
}
71+
72+
public function testCantInsertImageNotFoundNotAllowed(): void
73+
{
74+
$imagePath = 'https://phpspreadsheet.readthedocs.io/en/latest/topics/images/xxx01-03-filter-icon-1.png';
75+
$html = '<table>
76+
<tr>
77+
<td><img src="' . $imagePath . '" alt="test image voilà"></td>
78+
</tr>
79+
</table>';
80+
$filename = HtmlHelper::createHtml($html);
81+
$spreadsheet = HtmlHelper::loadHtmlIntoSpreadsheet($filename, true, true);
4882
$firstSheet = $spreadsheet->getSheet(0);
4983
$drawingCollection = $firstSheet->getDrawingCollection();
5084
self::assertCount(0, $drawingCollection);
85+
$spreadsheet->disconnectWorksheets();
5186
}
5287

5388
#[DataProvider('providerBadProtocol')]

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)