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

  1. 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.
  2. 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.
  3. Add upload validation that rejects any file whose first bytes or declaration indicate UTF-7 before it ever reaches the reader.
  4. 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

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


AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17). Data as JSON: /api/errors/7d63d3cc865e2eb9. Report an issue: GitHub.