{"record":{"id":"26af253cb822afcb","repo":"PHPOffice/PhpSpreadsheet","slug":"value","errorCode":"#VALUE!","errorMessage":"#VALUE!","messagePattern":"#VALUE!","errorType":"error_code","errorClass":"PhpOffice\\PhpSpreadsheet\\Calculation\\Exception","httpStatus":null,"severity":"critical","filePath":"src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php","lineNumber":101,"sourceCode":"\n        // Execute function\n        $excelDateValue = SharedDateHelper::formattedPHPToExcel($year, $month, $day);\n\n        return Helpers::returnIn3FormatsFloat($excelDateValue);\n    }\n\n    /**\n     * Convert year from multiple formats to int.\n     */\n    private static function getYear(mixed $year, int $baseYear): int\n    {\n        if ($year === null) {\n            $year = 0;\n        } elseif (is_scalar($year)) {\n            $year = StringHelper::testStringAsNumeric((string) $year);\n        }\n        if (!is_numeric($year)) {\n            throw new Exception(ExcelError::VALUE());\n        }\n        $year = (int) $year;\n\n        if ($year < ($baseYear - 1900)) {\n            throw new Exception(ExcelError::NAN());\n        }\n        if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) {\n            throw new Exception(ExcelError::NAN());\n        }\n\n        if (($year < $baseYear) && ($year >= ($baseYear - 1900))) {\n            $year += 1900;\n        }\n\n        return (int) $year;\n    }\n\n    /**","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php#L83-L119","documentation":"This is the second, post-conversion pass of the same guard: scan() re-runs the forbidden-pattern match (<!DOCTYPE for spreadsheet readers, <!ENTITY for the Html reader, NUL bytes tolerated between characters) after toUtf8() has normalized the payload to UTF-8. A hit only at this stage (XmlScanner.php:98-100) means the DOCTYPE/ENTITY construct was invisible in the raw bytes and only became ASCII-readable after charset conversion or after toUtf8() stripped the encoding declaration - the classic shape of an encoding-obfuscated XXE/XEE payload that defeats single-pass, byte-level filters.","triggerScenarios":"Loading a file in a non-UTF-8, non-ASCII-compatible encoding (so the marker is not byte-0x3C '<' in the raw stream) whose converted form contains <!DOCTYPE/<!ENTITY; payloads crafted so the first raw-byte check passes but mb_convert_encoding() to UTF-8 reveals the entity; documents where the encoding attribute removal inside toUtf8() exposes or completes the construct. Same entry points as error 403: IOFactory::load(), reader->load(), scan(), scanFile().","commonSituations":"Deliberate XXE attempts that encode the payload in UTF-16 or exotic charsets to slip naive scanners; regression/pen-test fixtures for CVE-style PhpSpreadsheet bypasses; rarely, a genuinely benign non-UTF-8 file that carries a DTD and survives conversion.","solutions":["Treat this signal as hostile by default: reject and quarantine the upload, and log the file for review - DOCTYPE/ENTITY hidden behind an encoding is not produced by legitimate spreadsheet tools.","If a trusted source genuinely emits it, decode to UTF-8 externally (mb_convert_encoding with the true source charset), strip <!DOCTYPE/<!ENTITY blocks, and load the sanitized copy.","Do not attempt to weaken XmlScanner or swap the pattern - the double check is what catches encoding-obfuscated payloads.","For user-supplied HTML/SVG (Html reader), preprocess with an HTML purifier that drops DTD/ENTITY constructs before PhpSpreadsheet sees the input."],"exampleFix":"// before\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load('upload.xlsx');\n// throws: Detected use of ENTITY in XML ... (payload hidden in non-UTF-8 encoding)\n\n// after - decode, sanitize, then load a rewritten copy\n$xml = file_get_contents('upload.xlsx'); // for zip-based xlsx, extract the offending part instead\n$xml = mb_convert_encoding($xml, 'UTF-8', mb_detect_encoding($xml) ?: 'UTF-8');\n$xml = preg_replace('/<!DOCTYPE[^>]*(\\[[^]]*\\])?>/s', '', $xml) ?? $xml;\n$tmp = tempnam(sys_get_temp_dir(), 'xlsx');\nfile_put_contents($tmp, $xml);\n$spreadsheet = \\PhpOffice\\PhpSpreadsheet\\IOFactory::load($tmp);","handlingStrategy":"try-catch","validationCode":"// Decode to UTF-8 first, then check for markers the raw scan could not see\nfunction sanitizedCopyForTrustedSources(string $path): string\n{\n    $raw = (string) file_get_contents($path);\n    $charset = mb_detect_encoding($raw, ['UTF-8', 'UTF-16LE', 'UTF-16BE'], true) ?: 'UTF-8';\n    $xml = $charset === 'UTF-8' ? $raw : mb_convert_encoding($raw, 'UTF-8', $charset);\n    return preg_replace('/<!DOCTYPE[^>]*(\\[[^]]*\\])?>/s', '', $xml) ?? $xml;\n}\n// Only use for trusted, non-adversarial inputs; for uploads, reject instead.","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(), 'ENTITY in XML')) {\n        // Marker appeared only after charset conversion => encoding-obfuscated payload.\n        // Quarantine + security-log the file; never retry the same bytes.\n    }\n    throw $e;\n}","preventionTips":["Default-deny: for user uploads, treat this exception as an attack, not a data problem.","Restrict accepted upload encodings to UTF-8/UTF-16-with-BOM and reject everything else before parsing.","Run untrusted HTML/SVG through an HTML purifier that removes DTD/ENTITY constructs before the Html reader sees it.","Keep PhpSpreadsheet current - encoding-obfuscation bypasses are exactly what post-conversion re-scanning was added for.","Monitor logs for repeat offenders (same source IP/upload endpoint) when this exception fires."],"tags":["php","phpspreadsheet","xml","xxe","xee","security","charset-obfuscation","entity-expansion"],"backgroundTag":"xxe-attack-blocked","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}