Skip to content

Commit 7c06eed

Browse files
authored
All Readers - Allow or Forbid Fetching of External Images Release129 (#4545)
* All Readers - Allow or Forbid Fetching of External Images 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 05b6c43 commit 7c06eed

File tree

9 files changed

+147
-12
lines changed

9 files changed

+147
-12
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 - 1.29.12
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 #4545](https://github.com/PHPOffice/PhpSpreadsheet/pull/4545)
13+
814
# 2025-06-22 - 1.29.11
915

1016
### Changed

src/PhpSpreadsheet/Reader/BaseReader.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ abstract class BaseReader implements IReader
4444
*/
4545
protected $loadSheetsOnly;
4646

47+
/**
48+
* Allow external images. Use with caution.
49+
* Improper specification of these within a spreadsheet
50+
* can subject the caller to security exploits.
51+
*
52+
* @var bool
53+
*/
54+
protected $allowExternalImages = true;
55+
4756
/**
4857
* IReadFilter instance.
4958
*
@@ -160,6 +169,12 @@ protected function processFlags(int $flags): void
160169
if (((bool) ($flags & self::SKIP_EMPTY_CELLS) || (bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
161170
$this->setReadEmptyCells(false);
162171
}
172+
if (((bool) ($flags & self::ALLOW_EXTERNAL_IMAGES)) === true) {
173+
$this->setAllowExternalImages(true);
174+
}
175+
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
176+
$this->setAllowExternalImages(false);
177+
}
163178
}
164179

165180
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
@@ -203,4 +218,21 @@ protected function openFile(string $filename): void
203218

204219
$this->fileHandle = $fileHandle;
205220
}
221+
222+
/**
223+
* Allow external images. Use with caution.
224+
* Improper specification of these within a spreadsheet
225+
* can subject the caller to security exploits.
226+
*/
227+
public function setAllowExternalImages(bool $allowExternalImages)
228+
{
229+
$this->allowExternalImages = $allowExternalImages;
230+
231+
return $this;
232+
}
233+
234+
public function getAllowExternalImages()
235+
{
236+
return $this->allowExternalImages;
237+
}
206238
}

src/PhpSpreadsheet/Reader/Html.php

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

10861086
$drawing = new Drawing();
1087-
$drawing->setPath($src, false);
1087+
$drawing->setPath($src, false, null, $this->allowExternalImages);
10881088
if ($drawing->getPath() === '') {
10891089
return;
10901090
}

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ interface IReader
1111
public const SKIP_EMPTY_CELLS = 4;
1212
public const IGNORE_EMPTY_CELLS = 4;
1313

14+
/**
15+
* Allow external images. Use with caution.
16+
* Improper specification of these within a spreadsheet
17+
* can subject the caller to security exploits.
18+
*/
19+
public const ALLOW_EXTERNAL_IMAGES = 16;
20+
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
21+
1422
/**
1523
* IReader constructor.
1624
*/
@@ -127,6 +135,22 @@ public function getReadFilter();
127135
*/
128136
public function setReadFilter(IReadFilter $readFilter);
129137

138+
/**
139+
* Allow external images. Use with caution.
140+
* Improper specification of these within a spreadsheet
141+
* can subject the caller to security exploits.
142+
*
143+
* @param bool $allowExternalImages
144+
*
145+
* @return IReader
146+
*/
147+
public function setAllowExternalImages(bool $allowExternalImages);
148+
149+
/**
150+
* @return bool
151+
*/
152+
public function getAllowExternalImages();
153+
130154
/**
131155
* Loads PhpSpreadsheet from file.
132156
*
@@ -136,6 +160,8 @@ public function setReadFilter(IReadFilter $readFilter);
136160
* self::READ_DATA_ONLY Read only data, not style or structure information, from the file
137161
* self::SKIP_EMPTY_CELLS Don't read empty cells (cells that contain a null value,
138162
* empty string, or a string containing only whitespace characters)
163+
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
164+
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
139165
*
140166
* @return \PhpOffice\PhpSpreadsheet\Spreadsheet
141167
*/

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,7 +1436,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
14361436
);
14371437
if (isset($images[$linkImageKey])) {
14381438
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1439-
$objDrawing->setPath($url, false);
1439+
$objDrawing->setPath($url, false, null, $this->allowExternalImages);
14401440
}
14411441
if ($objDrawing->getPath() === '') {
14421442
continue;
@@ -1525,7 +1525,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
15251525
);
15261526
if (isset($images[$linkImageKey])) {
15271527
$url = str_replace('xl/drawings/', '', $images[$linkImageKey]);
1528-
$objDrawing->setPath($url, false);
1528+
$objDrawing->setPath($url, false, null, $this->allowExternalImages);
15291529
}
15301530
if ($objDrawing->getPath() === '') {
15311531
continue;

src/PhpSpreadsheet/Worksheet/Drawing.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,11 @@ public function getPath()
101101
* @param string $path File path
102102
* @param bool $verifyFile Verify file
103103
* @param ZipArchive $zip Zip archive instance
104+
* @param bool $allowExternal
104105
*
105106
* @return $this
106107
*/
107-
public function setPath($path, $verifyFile = true, $zip = null)
108+
public function setPath($path, $verifyFile = true, $zip = null, $allowExternal = true)
108109
{
109110
$this->isUrl = false;
110111
if (preg_match('~^data:image/[a-z]+;base64,~', $path) === 1) {
@@ -119,6 +120,9 @@ public function setPath($path, $verifyFile = true, $zip = null)
119120
if (!preg_match('/^(http|https|file|ftp|s3):/', $path)) {
120121
throw new PhpSpreadsheetException('Invalid protocol for linked drawing');
121122
}
123+
if (!$allowExternal) {
124+
return $this;
125+
}
122126
// Implicit that it is a URL, rather store info than running check above on value in other places.
123127
$this->isUrl = true;
124128
$ctx = null;

tests/PhpSpreadsheetTests/Reader/Html/HtmlHelper.php

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

19-
public static function loadHtmlIntoSpreadsheet(string $filename, bool $unlink = false): Spreadsheet
19+
public static function loadHtmlIntoSpreadsheet(string $filename, bool $unlink = false, ?bool $allowExternalImages = null): Spreadsheet
2020
{
2121
$html = new Html();
22+
if ($allowExternalImages !== null) {
23+
$html->setAllowExternalImages($allowExternalImages);
24+
}
2225
$spreadsheet = $html->load($filename);
2326
if ($unlink) {
2427
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: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010

1111
class URLImageTest extends TestCase
1212
{
13-
public function testURLImageSource(): void
13+
public function testURLImageSourceAllowed(): void
1414
{
1515
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
1616
self::markTestSkipped('Skipped due to setting of environment variable');
1717
}
1818
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx');
1919
self::assertNotFalse($filename);
2020
$reader = IOFactory::createReader('Xlsx');
21+
$reader->setAllowExternalImages(true);
2122
$spreadsheet = $reader->load($filename);
2223
$worksheet = $spreadsheet->getActiveSheet();
2324
$collection = $worksheet->getDrawingCollection();
@@ -32,17 +33,47 @@ public function testURLImageSource(): void
3233
self::assertSame(84, $drawing->getWidth());
3334
self::assertSame(44, $drawing->getHeight());
3435
}
35-
$spreadsheet->disconnectWorksheets();
36+
$spreadsheet->disconnectWorksheets();
37+
}
38+
39+
public function testURLImageSourceNotAllowed(): void
40+
{
41+
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.xlsx');
42+
self::assertNotFalse($filename);
43+
$reader = IOFactory::createReader('Xlsx');
44+
$reader->setAllowExternalImages(false);
45+
$spreadsheet = $reader->load($filename);
46+
$worksheet = $spreadsheet->getActiveSheet();
47+
$collection = $worksheet->getDrawingCollection();
48+
self::assertCount(0, $collection);
49+
$spreadsheet->disconnectWorksheets();
50+
}
51+
52+
public function testURLImageSourceNotFoundAllowed(): void
53+
{
54+
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
55+
self::markTestSkipped('Skipped due to setting of environment variable');
56+
}
57+
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.notfound.xlsx');
58+
self::assertNotFalse($filename);
59+
$reader = IOFactory::createReader('Xlsx');
60+
$reader->setAllowExternalImages(true);
61+
$spreadsheet = $reader->load($filename);
62+
$worksheet = $spreadsheet->getActiveSheet();
63+
$collection = $worksheet->getDrawingCollection();
64+
self::assertCount(0, $collection);
65+
$spreadsheet->disconnectWorksheets();
3666
}
3767

38-
public function testURLImageSourceNotFound(): void
68+
public function testURLImageSourceNotFoundNotAllowed(): void
3969
{
4070
if (getenv('SKIP_URL_IMAGE_TEST') === '1') {
4171
self::markTestSkipped('Skipped due to setting of environment variable');
4272
}
4373
$filename = realpath(__DIR__ . '/../../../data/Reader/XLSX/urlImage.notfound.xlsx');
4474
self::assertNotFalse($filename);
4575
$reader = IOFactory::createReader('Xlsx');
76+
$reader->setAllowExternalImages(false);
4677
$spreadsheet = $reader->load($filename);
4778
$worksheet = $spreadsheet->getActiveSheet();
4879
$collection = $worksheet->getDrawingCollection();

0 commit comments

Comments
 (0)