{"record":{"id":"7d63d3cc865e2eb9","repo":"PHPOffice/PhpSpreadsheet","slug":"cloning-the-calculation-engine-is-not-allowed","errorCode":null,"errorMessage":"Cloning the calculation engine is not allowed!","messagePattern":"Cloning the calculation engine is not allowed!","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Calculation\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Calculation/Calculation.php","lineNumber":273,"sourceCode":"        $this->clearCalculationCache();\n        $this->branchPruner->clearBranchStore();\n        $this->formulaTokenCache = [];\n    }\n\n    /**\n     * Get the Logger for this calculation engine instance.\n     */\n    public function getDebugLog(): Logger\n    {\n        return $this->debugLog;\n    }\n\n    /**\n     * __clone implementation. Cloning should not be allowed in a Singleton!\n     */\n    final public function __clone()\n    {\n        throw new Exception('Cloning the calculation engine is not allowed!');\n    }\n\n    /**\n     * Set the Array Return Type (Array or Value of first element in the array).\n     *\n     * @param string $returnType Array return type\n     *\n     * @return bool Success or failure\n     */\n    public static function setArrayReturnType(string $returnType): bool\n    {\n        if (\n            ($returnType == self::RETURN_ARRAY_AS_VALUE)\n            || ($returnType == self::RETURN_ARRAY_AS_ERROR)\n            || ($returnType == self::RETURN_ARRAY_AS_ARRAY)\n        ) {\n            self::$returnArrayAsType = $returnType;\n","sourceCodeStart":255,"sourceCodeEnd":291,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Calculation/Calculation.php#L255-L291","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load('upload.xml');\n// throws: UTF-7 encoding not permitted\n\n// after - re-encode a trusted UTF-7 file to UTF-8, then load\n$xml = file_get_contents('upload.xml');\n$xml = mb_convert_encoding($xml, 'UTF-8', 'UTF-7');\n$xml = preg_replace('/encoding\\s*=\\s*([\"\\'])UTF-7\\1/i', 'encoding=\"UTF-8\"', $xml);\n$tmp = tempnam(sys_get_temp_dir(), 'xlsx');\nfile_put_contents($tmp, $xml);\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load($tmp);","handlingStrategy":"validation","validationCode":"// Reject UTF-7 at the boundary, before PhpSpreadsheet touches the file\nfunction isUtf7Xml(string $path): bool\n{\n    $head = (string) file_get_contents($path, false, null, 0, 512);\n    return (bool) preg_match('/encoding\\s*=\\s*([\"\\'])UTF-7\\1/i', $head)\n        || str_contains(strtoupper(bin2hex(substr($head, 0, 8))), '2B414457'); // '+ADW' UTF-7 '<'\n}\n\nif (isUtf7Xml($uploadPath)) {\n    http_response_code(415);\n    exit('UTF-7 encoded files are not accepted. Please re-save as UTF-8.');\n}","typeGuard":null,"tryCatchPattern":"use PhpOffice\\PhpSpreadsheet\\Reader\\Exception as ReaderException;\n\ntry {\n    $spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load($path);\n} catch (ReaderException $e) {\n    if (str_contains($e->getMessage(), 'UTF-7 encoding not permitted')) {\n        // security-relevant: log the upload, quarantine it, never auto-retry with the same bytes\n        $quarantine->store($path, 'utf7-xml');\n    }\n    throw $e;\n}","preventionTips":["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."],"tags":["php","phpspreadsheet","xml","utf-7","security","xxe","file-upload"],"backgroundTag":"xxe-attack-blocked","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}