Skip to content

Commit 31d7f79

Browse files
committed
Backport Security Patch
1 parent 15e028f commit 31d7f79

File tree

20 files changed

+293
-110
lines changed

20 files changed

+293
-110
lines changed

.github/workflows/main.yml

+21-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
- '8.1'
1414
- '8.2'
1515
- '8.3'
16+
- '8.4'
1617

1718
include:
1819
- php-version: 'nightly'
@@ -44,7 +45,7 @@ jobs:
4445

4546
- name: Delete composer lock file
4647
id: composer-lock
47-
if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' || matrix.php-version == '8.3' || matrix.php-version == 'nightly'}}
48+
if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' || matrix.php-version == '8.3' || matrix.php-version == '8.4' || matrix.php-version == 'nightly'}}
4849
run: |
4950
rm composer.lock
5051
echo "flags=--ignore-platform-reqs" >> $GITHUB_OUTPUT
@@ -67,9 +68,27 @@ jobs:
6768
- name: "Run PHPUnit tests 2 (Experimental: ${{ matrix.experimental }})"
6869
env:
6970
FAILURE_ACTION: "${{ matrix.experimental == true }}"
70-
if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' || matrix.php-version == '8.3' || matrix.php-version == 'nightly'}}
71+
if: ${{ matrix.php-version == '8.1' || matrix.php-version == '8.2' || matrix.php-version == '8.3' || matrix.php-version == '8.4' || matrix.php-version == 'nightly'}}
7172
run: vendor/bin/phpunit --verbose || $FAILURE_ACTION
7273

74+
find-polyfill:
75+
runs-on: ubuntu-latest
76+
steps:
77+
- name: Checkout
78+
uses: actions/checkout@v4
79+
with:
80+
fetch-depth: 2
81+
82+
- name: Setup PHP, with composer and extensions
83+
uses: shivammathur/setup-php@v2
84+
with:
85+
php-version: 8.3
86+
extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib
87+
coverage: none
88+
89+
- name: Find code that might require polyfill
90+
run: php ./bin/findpolyfill.php
91+
7392
php-cs-fixer:
7493
runs-on: ubuntu-latest
7594
steps:

CHANGELOG.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,18 @@ 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 - 1.29.3
8+
## 1.29.3 - 2024-11-10
9+
10+
### Fixed
11+
12+
- Backported security patches.
13+
- Write ignoredErrors Tag Before Drawings. Backport of [PR #4212](https://github.com/PHPOffice/PhpSpreadsheet/pull/4212) intended for 3.4.0.
14+
- Changes to ROUNDDOWN/ROUNDUP/TRUNC. Backport of [PR #4214](https://github.com/PHPOffice/PhpSpreadsheet/pull/4214) intended for 3.4.0.
15+
- Replace str_starts_with in Drawing. [Issue #4215](https://github.com/PHPOffice/PhpSpreadsheet/issues/4215)
916

1017
### Added
1118

12-
- Method to Test Whether Csv Will Be Affected by Php9 (backport of PR #4189 intended for 3.4.0)
19+
- Method to Test Whether Csv Will Be Affected by Php9. Backport of [PR #4189](https://github.com/PHPOffice/PhpSpreadsheet/pull/4189) intended for 3.4.0.
1320

1421
## 1.29.2 - 2024-09-29
1522

bin/findpolyfill.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
function findPolyfill(string $directory): int
5+
{
6+
// See issue #4215 - code which should have erred in unit test
7+
// succeeded because a dev package required polyfills.
8+
$retCode = 0;
9+
$polyfill80 = 'FILTER_VALIDATE_BOOL\\b'
10+
. '|fdiv[(]'
11+
. '|preg_last_error_msg[(]'
12+
. '|str_contains[(]'
13+
. '|str_starts_with[(]'
14+
. '|str_ends_with[(]'
15+
. '|get_debug_type[(]'
16+
. '|get_resource_id[(]';
17+
$polyfill81 = 'MYSQLI_REFRESH_REPLICA\\b'
18+
. '|fdiv[(]'
19+
. '|array_is_list[(]'
20+
. '|enum_exists[(]';
21+
$polyfill = '/\\b(?:'
22+
. $polyfill80
23+
. '|'
24+
. $polyfill81
25+
. '})/';
26+
27+
$it = new RecursiveIteratorIterator(
28+
new RecursiveDirectoryIterator($directory, FilesystemIterator::UNIX_PATHS)
29+
);
30+
31+
foreach ($it as $file) {
32+
if ($file->getExtension() === 'php') {
33+
$fullname = $it->getPath() . '/' . $it->getBaseName();
34+
$contents = file_get_contents($fullname);
35+
if ($contents === false) {
36+
echo "failed to read $fullname\n";
37+
++$retCode;
38+
} elseif (preg_match_all($polyfill, $contents, $matches)) {
39+
var_dump($fullname, $matches);
40+
++$retCode;
41+
}
42+
}
43+
}
44+
45+
return $retCode;
46+
}
47+
48+
// Don't care if tests use polyfill
49+
$errors = findPolyfill(__DIR__ . '/../src') + findPolyfill(__DIR__ . '/../samples');
50+
if ($errors !== 0) {
51+
echo "Found $errors files that might require polyfills\n";
52+
exit(1);
53+
}
54+
echo "No polyfills needed\n";

src/PhpSpreadsheet/Calculation/MathTrig/Round.php

+24-29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
66
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
77
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
8+
// following added in Php8.4
9+
use RoundingMode;
810

911
class Round
1012
{
@@ -67,31 +69,28 @@ public static function up($number, $digits)
6769
return 0.0;
6870
}
6971

70-
$digitsPlus1 = $digits + 1;
71-
if ($number < 0.0) {
72-
if ($digitsPlus1 < 0) {
73-
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
74-
}
75-
$result = sprintf("%.{$digitsPlus1}f", $number - 0.5 * 0.1 ** $digits);
76-
77-
return round((float) $result, $digits, PHP_ROUND_HALF_DOWN);
72+
if (PHP_VERSION_ID >= 80400) {
73+
return round(
74+
(float) (string) $number,
75+
$digits,
76+
RoundingMode::AwayFromZero //* @phpstan-ignore-line
77+
);
7878
}
7979

80-
if ($digitsPlus1 < 0) {
81-
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
80+
if ($number < 0.0) {
81+
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
8282
}
83-
$result = sprintf("%.{$digitsPlus1}f", $number + 0.5 * 0.1 ** $digits);
8483

85-
return round((float) $result, $digits, PHP_ROUND_HALF_DOWN);
84+
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN);
8685
}
8786

8887
/**
8988
* ROUNDDOWN.
9089
*
9190
* Rounds a number down to a specified number of decimal places
9291
*
93-
* @param array|float $number Number to round, or can be an array of numbers
94-
* @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers
92+
* @param null|array|float|string $number Number to round, or can be an array of numbers
93+
* @param array|float|int|string $digits Number of digits to which you want to round $number, or can be an array of numbers
9594
*
9695
* @return array|float|string Rounded Number, or a string containing an error
9796
* If an array of numbers is passed as the argument, then the returned result will also be an array
@@ -114,23 +113,19 @@ public static function down($number, $digits)
114113
return 0.0;
115114
}
116115

117-
$digitsPlus1 = $digits + 1;
118-
if ($number < 0.0) {
119-
if ($digitsPlus1 < 0) {
120-
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
121-
}
122-
$result = sprintf("%.{$digitsPlus1}f", $number + 0.5 * 0.1 ** $digits);
123-
124-
return round((float) $result, $digits, PHP_ROUND_HALF_UP);
116+
if (PHP_VERSION_ID >= 80400) {
117+
return round(
118+
(float) (string) $number,
119+
$digits,
120+
RoundingMode::TowardsZero //* @phpstan-ignore-line
121+
);
125122
}
126123

127-
if ($digitsPlus1 < 0) {
128-
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
124+
if ($number < 0.0) {
125+
return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
129126
}
130127

131-
$result = sprintf("%.{$digitsPlus1}f", $number - 0.5 * 0.1 ** $digits);
132-
133-
return round((float) $result, $digits, PHP_ROUND_HALF_UP);
128+
return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP);
134129
}
135130

136131
/**
@@ -141,7 +136,7 @@ public static function down($number, $digits)
141136
* @param mixed $number Expect float. Number to round, or can be an array of numbers
142137
* @param mixed $multiple Expect int. Multiple to which you want to round, or can be an array of numbers.
143138
*
144-
* @return array|float|string Rounded Number, or a string containing an error
139+
* @return array|float|int|string Rounded Number, or a string containing an error
145140
* If an array of numbers is passed as the argument, then the returned result will also be an array
146141
* with the same dimensions
147142
*/
@@ -210,7 +205,7 @@ public static function even($number)
210205
*
211206
* @param array|float $number Number to round, or can be an array of numbers
212207
*
213-
* @return array|float|string Rounded Number, or a string containing an error
208+
* @return array|float|int|string Rounded Number, or a string containing an error
214209
* If an array of numbers is passed as the argument, then the returned result will also be an array
215210
* with the same dimensions
216211
*/

src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php

+8-33
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
44

55
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
6-
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
76

87
class Trunc
98
{
@@ -13,11 +12,14 @@ class Trunc
1312
* TRUNC.
1413
*
1514
* Truncates value to the number of fractional digits by number_digits.
15+
* This will probably not be the precise result in the unlikely
16+
* event that the number of digits to the left of the decimal
17+
* plus the number of digits to the right exceeds PHP_FLOAT_DIG
18+
* (or possibly that value minus 1).
19+
* Excel is unlikely to do any better.
1620
*
17-
* @param array|float $value
18-
* Or can be an array of values
19-
* @param array|int $digits
20-
* Or can be an array of values
21+
* @param null|array|float|string $value Or can be an array of values
22+
* @param array|float|int|string $digits Or can be an array of values
2123
*
2224
* @return array|float|string Truncated value, or a string containing an error
2325
* If an array of numbers is passed as an argument, then the returned result will also be an array
@@ -29,33 +31,6 @@ public static function evaluate($value = 0, $digits = 0)
2931
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits);
3032
}
3133

32-
try {
33-
$value = Helpers::validateNumericNullBool($value);
34-
$digits = Helpers::validateNumericNullSubstitution($digits, null);
35-
} catch (Exception $e) {
36-
return $e->getMessage();
37-
}
38-
39-
if ($value == 0) {
40-
return $value;
41-
}
42-
43-
if ($value >= 0) {
44-
$minusSign = '';
45-
} else {
46-
$minusSign = '-';
47-
$value = -$value;
48-
}
49-
50-
$digits = (int) floor($digits);
51-
if ($digits < 0) {
52-
$power = (int) (10 ** -$digits);
53-
$result = intdiv((int) floor($value), $power) * $power;
54-
return ($minusSign === '') ? $result : -$result;
55-
}
56-
$digitsPlus1 = $digits + 1;
57-
$result = substr($minusSign . sprintf("%.{$digitsPlus1}f", $value), 0, -1);
58-
59-
return (float) $result;
34+
return Round::down($value, $digits);
6035
}
6136
}

src/PhpSpreadsheet/Reader/Security/XmlScanner.php

+36-18
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
/**
1013
* String used to identify risky xml elements.
1114
*
@@ -114,29 +117,41 @@ private static function forceString($arg): string
114117
private function toUtf8($xml)
115118
{
116119
$charset = $this->findCharSet($xml);
120+
$foundUtf7 = $charset === 'UTF-7';
117121
if ($charset !== 'UTF-8') {
122+
$testStart = '/^.{0,4}\\s*<?xml/s';
123+
$startWithXml1 = preg_match($testStart, $xml);
118124
$xml = self::forceString(mb_convert_encoding($xml, 'UTF-8', $charset));
119-
120-
$charset = $this->findCharSet($xml);
121-
if ($charset !== 'UTF-8') {
122-
throw new Reader\Exception('Suspicious Double-encoded XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
125+
if ($startWithXml1 === 1 && preg_match($testStart, $xml) !== 1) {
126+
throw new Reader\Exception('Double encoding not permitted');
123127
}
128+
$foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
129+
$xml = preg_replace(self::ENCODING_PATTERN, '', $xml) ?? $xml;
130+
} else {
131+
$foundUtf7 = $foundUtf7 || (preg_match(self::ENCODING_UTF7, $xml) === 1);
132+
}
133+
if ($foundUtf7) {
134+
throw new Reader\Exception('UTF-7 encoding not permitted');
135+
}
136+
if (substr($xml, 0, Reader\Csv::UTF8_BOM_LEN) === Reader\Csv::UTF8_BOM) {
137+
$xml = substr($xml, Reader\Csv::UTF8_BOM_LEN);
124138
}
125139

126140
return $xml;
127141
}
128142

129143
private function findCharSet(string $xml): string
130144
{
131-
$patterns = [
132-
'/encoding\\s*=\\s*"([^"]*]?)"/',
133-
"/encoding\\s*=\\s*'([^']*?)'/",
134-
];
135-
136-
foreach ($patterns as $pattern) {
137-
if (preg_match($pattern, $xml, $matches)) {
138-
return strtoupper($matches[1]);
139-
}
145+
if (substr($xml, 0, 4) === "\x4c\x6f\xa7\x94") {
146+
throw new Reader\Exception('EBCDIC encoding not permitted');
147+
}
148+
$encoding = Reader\Csv::guessEncodingBom('', $xml);
149+
if ($encoding !== '') {
150+
return $encoding;
151+
}
152+
$xml = str_replace("\0", '', $xml);
153+
if (preg_match(self::ENCODING_PATTERN, $xml, $matches)) {
154+
return strtoupper($matches[2]);
140155
}
141156

142157
return 'UTF-8';
@@ -151,13 +166,16 @@ private function findCharSet(string $xml): string
151166
*/
152167
public function scan($xml)
153168
{
154-
$xml = "$xml";
155169
$this->disableEntityLoaderCheck();
170+
// Don't rely purely on libxml_disable_entity_loader()
171+
$pattern = '/\\0*' . implode('\\0*', /** @scrutinizer ignore-type */ str_split($this->pattern)) . '\\0*/';
156172

157-
$xml = $this->toUtf8($xml);
173+
$xml = "$xml";
174+
if (preg_match($pattern, $xml)) {
175+
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
176+
}
158177

159-
// Don't rely purely on libxml_disable_entity_loader()
160-
$pattern = '/\\0?' . implode('\\0?', /** @scrutinizer ignore-type */ str_split($this->pattern)) . '\\0?/';
178+
$xml = $this->toUtf8($xml);
161179

162180
if (preg_match($pattern, $xml)) {
163181
throw new Reader\Exception('Detected use of ENTITY in XML, spreadsheet file load() aborted to prevent XXE/XEE attacks');
@@ -171,7 +189,7 @@ public function scan($xml)
171189
}
172190

173191
/**
174-
* Scan theXML for use of <!ENTITY to prevent XXE/XEE attacks.
192+
* Scan the XML for use of <!ENTITY to prevent XXE/XEE attacks.
175193
*
176194
* @param string $filestream
177195
*

0 commit comments

Comments
 (0)