Skip to content

Commit 0cf7292

Browse files
authored
Merge branch 'master' into fix_s_tag
2 parents de6bcaa + 3e52499 commit 0cf7292

File tree

14 files changed

+117
-46
lines changed

14 files changed

+117
-46
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ jobs:
210210
runs-on: ubuntu-latest
211211
steps:
212212
- name: Checkout
213-
uses: actions/checkout@v3
213+
uses: actions/checkout@v4
214214
with:
215215
fetch-depth: 0
216216

CHANGELOG.md

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,39 @@ 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-
## TBD - 3.4.0
8+
## TBD - 3.5.0
9+
10+
### Added
11+
12+
- Nothing yet.
13+
14+
### Changed
15+
16+
- Nothing yet.
17+
18+
### Moved
19+
20+
- Nothing yet.
21+
22+
### Deprecated
23+
24+
- Nothing yet.
25+
26+
### Fixed
27+
28+
- Nothing yet.
29+
30+
## 2024-11-10 - 3.4.0
31+
32+
### Security Fix
33+
34+
- Several security patches.
935

1036
### Added
1137

1238
- Add Dynamic valueBinder Property to Spreadsheet and Readers. [Issue #1395](https://github.com/PHPOffice/PhpSpreadsheet/issues/1395) [PR #4185](https://github.com/PHPOffice/PhpSpreadsheet/pull/4185)
1339
- Allow Omitting Chart Border. [Issue #562](https://github.com/PHPOffice/PhpSpreadsheet/issues/562) [PR #4188](https://github.com/PHPOffice/PhpSpreadsheet/pull/4188)
14-
- Method to Test Whether Csv Will Be Affected by Php0. [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189)
40+
- Method to Test Whether Csv Will Be Affected by Php9. [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189)
1541

1642
### Changed
1743

CONTRIBUTING.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ If you would like to contribute, here are some notes and guidelines:
44

55
- All new development should be on feature/fix branches, which are then merged to the `master` branch once stable and approved; so the `master` branch is always the most up-to-date, working code
66
- If you are going to submit a pull request, please fork from `master`, and submit your pull request back as a fix/feature branch referencing the GitHub issue number
7+
- Install (development) dependencies by running `composer install` inside your PhpSpreadsheet clone.
78
- The code must work with all PHP versions that we support.
89
- You can call `composer versions` to test version compatibility.
910
- Code style should be maintained.
@@ -39,7 +40,10 @@ This makes it easier to see exactly what is being tested when reviewing the PR.
3940
2. Tag subject must be the version number, eg: `1.2.3`
4041
3. Tag body must be a copy-paste of the changelog entries.
4142
3. Push the tag with `git push --tags`, GitHub Actions will create a GitHub release automatically, and the release details will automatically be sent to packagist.
42-
4. Github seems to remove markdown headings in the Release Notes, so you should edit to restore these.
43+
4. By default, Github remove markdown headings in the Release Notes. You can either edit to restore these, or, probably preferably, change the default comment character on your system - `git config core.commentChar ';'`.
4344

44-
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.)
45+
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) However, there are 3 branches which have been updated to apply security patches, and those may be tagged if future security updates are needed.
46+
- release1291
47+
- release210
48+
- release222
4549

composer.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@
4545
],
4646
"scripts": {
4747
"check": [
48-
"./bin/check-phpdoc-types",
48+
"php ./bin/check-phpdoc-types",
4949
"phpcs samples/ src/ tests/ --report=checkstyle",
50-
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n",
50+
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n",
5151
"php-cs-fixer fix --ansi --dry-run --diff",
52-
"phpunit --color=always",
53-
"phpstan analyse --ansi --memory-limit=2048M"
52+
"phpstan analyse --ansi --memory-limit=2048M",
53+
"phpunit --color=always"
5454
],
5555
"style": [
5656
"phpcs samples/ src/ tests/ --report=checkstyle",
@@ -61,7 +61,7 @@
6161
"php-cs-fixer fix"
6262
],
6363
"versions": [
64-
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- -n"
64+
"phpcs samples/ src/ tests/ --standard=PHPCompatibility --runtime-set testVersion 8.0- --exclude=PHPCompatibility.Variables.ForbiddenThisUseContexts -n"
6565
]
6666
},
6767
"require": {

src/PhpSpreadsheet/Reader/Security/XmlScanner.php

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
class XmlScanner
88
{
9+
private const ENCODING_PATTERN = '/encoding\\s*=\\s*(["\'])(.+?)\\1/s';
10+
private const ENCODING_UTF7 = '/encoding\\s*=\\s*(["\'])UTF-7\\1/si';
11+
912
private string $pattern;
1013

1114
/** @var ?callable */
@@ -36,29 +39,41 @@ private static function forceString(mixed $arg): string
3639
private function toUtf8(string $xml): string
3740
{
3841
$charset = $this->findCharSet($xml);
42+
$foundUtf7 = $charset === 'UTF-7';
3943
if ($charset !== 'UTF-8') {
44+
$testStart = '/^.{0,4}\\s*<?xml/s';
45+
$startWithXml1 = preg_match($testStart, $xml);
4046
$xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
41-
42-
$charset = $this->findCharSet($xml);
43-
if ($charset !== 'UTF-8') {
44-
throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
47+
if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) {
48+
throw new Reader\Exception('Double encoding not permitted');
4549
}
50+
$foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
51+
$xml = preg_replace(self::ENCODING_PATTERN, '', $xml) ?? $xml;
52+
} else {
53+
$foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
54+
}
55+
if ($foundUtf7) {
56+
throw new Reader\Exception('UTF-7 encoding not permitted');
57+
}
58+
if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) {
59+
$xml = substr($xml, Reader\Csv::UTF8_BOM_LEN);
4660
}
4761

4862
return $xml;
4963
}
5064

5165
private function findCharSet(string $xml): string
5266
{
53-
$patterns = [
54-
'/encoding\\s*=\\s*"([^"]*]?)"/',
55-
"/encoding\\s*=\\s*'([^']*?)'/",
56-
];
57-
58-
foreach ($patterns as $pattern) {
59-
if (preg_match($pattern, $xml, $matches)) {
60-
return strtoupper($matches[1]);
61-
}
67+
if (substr($xml, 0, 4) === "\x4c\x6f\xa7\x94") {
68+
throw new Reader\Exception('EBCDIC encoding not permitted');
69+
}
70+
$encoding = Reader\Csv::guessEncodingBom('', $xml);
71+
if ($encoding !== '') {
72+
return $encoding;
73+
}
74+
$xml = str_replace("\0", '', $xml);
75+
if (preg_match(self::ENCODING_PATTERN, $xml, $matches)) {
76+
return strtoupper($matches[2]);
6277
}
6378

6479
return 'UTF-8';
@@ -71,13 +86,15 @@ private function findCharSet(string $xml): string
7186
*/
7287
public function scan($xml): string
7388
{
89+
// Don't rely purely on libxml_disable_entity_loader()
90+
$pattern = '/\\0*' . implode('\\0*', str_split($this->pattern)) . '\\0*/';
91+
7492
$xml = "$xml";
93+
if (preg_match($pattern, $xml)) {
94+
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
95+
}
7596

7697
$xml = $this->toUtf8($xml);
77-
78-
// Don't rely purely on libxml_disable_entity_loader()
79-
$pattern = '/\\0?' . implode('\\0?', str_split($this->pattern)) . '\\0?/';
80-
8198
if (preg_match($pattern, $xml)) {
8299
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
83100
}
@@ -90,7 +107,7 @@ public function scan($xml): string
90107
}
91108

92109
/**
93-
* Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
110+
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
94111
*/
95112
public function scanFile(string $filestream): string
96113
{

src/PhpSpreadsheet/Reader/Xml.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
use PhpOffice\PhpSpreadsheet\Settings;
1919
use PhpOffice\PhpSpreadsheet\Shared\Date;
2020
use PhpOffice\PhpSpreadsheet\Shared\File;
21-
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
2221
use PhpOffice\PhpSpreadsheet\Spreadsheet;
2322
use PhpOffice\PhpSpreadsheet\Worksheet\SheetView;
2423
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
@@ -91,7 +90,8 @@ public function canRead(string $filename): bool
9190
];
9291

9392
// Open file
94-
$data = file_get_contents($filename) ?: '';
93+
$data = (string) file_get_contents($filename);
94+
$data = $this->getSecurityScannerOrThrow()->scan($data);
9595

9696
// Why?
9797
//$data = str_replace("'", '"', $data); // fix headers with single quote
@@ -106,14 +106,6 @@ public function canRead(string $filename): bool
106106
}
107107
}
108108

109-
// Retrieve charset encoding
110-
if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m', $data, $matches)) {
111-
$charSet = strtoupper($matches[1]);
112-
if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) {
113-
$data = StringHelper::convertEncoding($data, 'UTF-8', $charSet);
114-
$data = (string) preg_replace('/(<?xml.*encoding=[\'"]).*?([\'"].*?>)/um', '$1' . 'UTF-8' . '$2', $data, 1);
115-
}
116-
}
117109
$this->fileContents = $data;
118110

119111
return $valid;

tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ public static function providerValidXML(): array
3030
self::assertNotFalse($glob);
3131
foreach ($glob as $file) {
3232
$filename = realpath($file);
33-
$expectedResult = file_get_contents($file);
33+
$expectedResult = (string) file_get_contents($file);
34+
if (preg_match('/UTF-16(LE|BE)?/', $file, $matches) == 1) {
35+
$expectedResult = (string) mb_convert_encoding($expectedResult, 'UTF-8', $matches[0]);
36+
$expectedResult = preg_replace('/encoding\\s*=\\s*[\'"]UTF-\\d+(LE|BE)?[\'"]/', '', $expectedResult) ?? $expectedResult;
37+
}
3438
$tests[basename($file)] = [$filename, $expectedResult];
3539
}
3640

@@ -132,19 +136,47 @@ public function testEncodingAllowsMixedCase(): void
132136
self::assertSame($input, $output);
133137
}
134138

135-
public function testUtf7Whitespace(): void
139+
/**
140+
* @dataProvider providerInvalidXlsx
141+
*/
142+
public function testInvalidXlsx(string $filename, string $message): void
136143
{
137144
$this->expectException(ReaderException::class);
138-
$this->expectExceptionMessage('Double-encoded');
145+
$this->expectExceptionMessage($message);
139146
$reader = new Xlsx();
140-
$reader->load('tests/data/Reader/XLSX/utf7white.dontuse');
147+
$reader->load("tests/data/Reader/XLSX/$filename");
141148
}
142149

143-
public function testUtf8Entity(): void
150+
public static function providerInvalidXlsx(): array
151+
{
152+
return [
153+
['utf7white.dontuse', 'UTF-7 encoding not permitted'],
154+
['utf7quoteorder.dontuse', 'UTF-7 encoding not permitted'],
155+
['utf8and16.dontuse', 'Double encoding not permitted'],
156+
['utf8and16.entity.dontuse', 'Detected use of ENTITY'],
157+
['utf8entity.dontuse', 'Detected use of ENTITY'],
158+
['utf16entity.dontuse', 'Detected use of ENTITY'],
159+
['ebcdic.dontuse', 'EBCDIC encoding not permitted'],
160+
];
161+
}
162+
163+
/**
164+
* @dataProvider providerValidUtf16
165+
*/
166+
public function testValidUtf16(string $filename): void
144167
{
145-
$this->expectException(ReaderException::class);
146-
$this->expectExceptionMessage('Detected use of ENTITY');
147168
$reader = new Xlsx();
148-
$reader->load('tests/data/Reader/XLSX/utf8entity.dontuse');
169+
$spreadsheet = $reader->load("tests/data/Reader/XLSX/$filename");
170+
$sheet = $spreadsheet->getActiveSheet();
171+
self::assertSame(1, $sheet->getCell('A1')->getValue());
172+
$spreadsheet->disconnectWorksheets();
173+
}
174+
175+
public static function providerValidUtf16(): array
176+
{
177+
return [
178+
['utf16be.xlsx'],
179+
['utf16be.bom.xlsx'],
180+
];
149181
}
150182
}

tests/data/Reader/XLSX/ebcdic.dontuse

5.45 KB
Binary file not shown.
7.93 KB
Binary file not shown.

tests/data/Reader/XLSX/utf16be.xlsx

7.93 KB
Binary file not shown.

0 commit comments

Comments
 (0)