PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Calculation\Exception
Cloning the calculation engine is not allowed!
Error message
Cloning the calculation engine is not allowed!
What it means
XmlScanner refuses to process any XML that is, or declares itself to be, UTF-7. UTF-7 can encode characters like '<' as '+ADw-', which lets an attacker hide a <!DOCTYPE/<!ENTITY (XXE) payload from byte-pattern scans, so PhpSpreadsheet treats it as a hostile encoding. The check runs both on the charset detected by findCharSet() and again after conversion to UTF-8 via the ENCODING_UTF7 regex '/encoding\s*=\s*(["\'])UTF-7\1/si'; either hit throws 'UTF-7 encoding not permitted' at XmlScanner.php:56.
Source
Thrown at src/PhpSpreadsheet/Calculation/Calculation.php:273
$this->clearCalculationCache();
$this->branchPruner->clearBranchStore();
$this->formulaTokenCache = [];
}
/**
* Get the Logger for this calculation engine instance.
*/
public function getDebugLog(): Logger
{
return $this->debugLog;
}
/**
* __clone implementation. Cloning should not be allowed in a Singleton!
*/
final public function __clone()
{
throw new Exception('Cloning the calculation engine is not allowed!');
}
/**
* Set the Array Return Type (Array or Value of first element in the array).
*
* @param string $returnType Array return type
*
* @return bool Success or failure
*/
public static function setArrayReturnType(string $returnType): bool
{
if (
($returnType == self::RETURN_ARRAY_AS_VALUE)
|| ($returnType == self::RETURN_ARRAY_AS_ERROR)
|| ($returnType == self::RETURN_ARRAY_AS_ARRAY)
) {
self::$returnArrayAsType = $returnType;
View on GitHub (pinned to 65b080eef4)
Solutions
- If the file is trusted, re-encode it to UTF-8 externally: mb_convert_encoding($xml,'UTF-8','UTF-7'), remove the encoding="UTF-7" declaration, then load the rewritten file.
- If it is not trusted, reject it: UTF-7 has no legitimate use in spreadsheet XML, so treat the exception as an attack indicator and quarantine/log the upload.
- Add upload validation that rejects any file whose first bytes or declaration indicate UTF-7 before it ever reaches the reader.
- Keep PhpSpreadsheet current - this hard block was added as a security fix, so older versions silently parsed UTF-7 instead of throwing.
Example fix
// before
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load('upload.xml');
// throws: UTF-7 encoding not permitted
// after - re-encode a trusted UTF-7 file to UTF-8, then load
$xml = file_get_contents('upload.xml');
$xml = mb_convert_encoding($xml, 'UTF-8', 'UTF-7');
$xml = preg_replace('/encoding\s*=\s*(["\'])UTF-7\1/i', 'encoding="UTF-8"', $xml);
$tmp = tempnam(sys_get_temp_dir(), 'xlsx');
file_put_contents($tmp, $xml);
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($tmp); Defensive patterns
Strategy: validation
Validate before calling
// Reject UTF-7 at the boundary, before PhpSpreadsheet touches the file
function isUtf7Xml(string $path): bool
{
$head = (string) file_get_contents($path, false, null, 0, 512);
return (bool) preg_match('/encoding\s*=\s*(["\'])UTF-7\1/i', $head)
|| str_contains(strtoupper(bin2hex(substr($head, 0, 8))), '2B414457'); // '+ADW' UTF-7 '<'
}
if (isUtf7Xml($uploadPath)) {
http_response_code(415);
exit('UTF-7 encoded files are not accepted. Please re-save as UTF-8.');
} Try / catch
use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException;
try {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path);
} catch (ReaderException $e) {
if (str_contains($e->getMessage(), 'UTF-7 encoding not permitted')) {
// security-relevant: log the upload, quarantine it, never auto-retry with the same bytes
$quarantine->store($path, 'utf7-xml');
}
throw $e;
} Prevention
- Treat any UTF-7 declaration in a spreadsheet/XML upload as malicious - no mainstream tool emits UTF-7 spreadsheet XML.
- Reject unexpected encodings at the upload layer (whitelist UTF-8, UTF-8-BOM, UTF-16 with BOM) instead of catching later.
- Log and alert on this specific exception; it frequently indicates active XXE probing of your import endpoint.
- Keep PhpSpreadsheet patched so scanner hardening fixes arrive automatically.
When it happens
Trigger: Calling IOFactory::load()/scan()/scanFile() on XML whose declaration contains encoding="UTF-7" (any case, single or double quotes), on a file that actually carries UTF-7-encoded bytes, or on a payload that still declares UTF-7 after mb_convert_encoding() to UTF-8 (the declaration itself survives conversion, so the post-conversion regex fires even if the body is harmless).
Common situations: Malicious uploads crafted to bypass the scanner (classic XXE vector against older PhpSpreadsheet); legacy files produced by old mail/calendar systems that emitted UTF-7; test/penetration fixtures; files mangled by a conversion pipeline that set encoding="UTF-7" in the declaration.
Related errors
- Locale file not found
- #VALUE!
- Unsupported binary comparison operator
- Unsupported numeric binary operation
- #NUM!
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/7d63d3cc865e2eb9.
Report an issue: GitHub.