{"record":{"id":"900aa728840e134b","repo":"PHPOffice/PhpSpreadsheet","slug":"security-scanner-is-unexpectedly-null","errorCode":null,"errorMessage":"Security scanner is unexpectedly null","messagePattern":"Security scanner is unexpectedly null","errorType":"exception","errorClass":"PhpOffice\\PhpSpreadsheet\\Reader\\Exception","httpStatus":null,"severity":"error","filePath":"src/PhpSpreadsheet/Reader/BaseReader.php","lineNumber":241,"sourceCode":"     * Create a blank sheet if none are read,\n     * possibly due to a typo when using LoadSheetsOnly.\n     */\n    public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): static\n    {\n        $this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;\n\n        return $this;\n    }\n\n    public function getSecurityScanner(): ?XmlScanner\n    {\n        return $this->securityScanner;\n    }\n\n    public function getSecurityScannerOrThrow(): XmlScanner\n    {\n        if ($this->securityScanner === null) {\n            throw new ReaderException('Security scanner is unexpectedly null');\n        }\n\n        return $this->securityScanner;\n    }\n\n    protected function processFlags(int $flags): void\n    {\n        if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) {\n            $this->setIncludeCharts(true);\n        }\n        if (((bool) ($flags & self::READ_DATA_ONLY)) === true) {\n            $this->setReadDataOnly(true);\n        }\n        if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {\n            $this->setReadEmptyCells(false);\n        }\n        if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {\n            $this->setIgnoreRowsWithNoCells(true);","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/PHPOffice/PhpSpreadsheet/blob/65b080eef4d9fd11a5796135ab145883e5c3d6a6/src/PhpSpreadsheet/Reader/BaseReader.php#L223-L259","documentation":"Every stock XML-based reader (Xlsx, Ods, Html, Gnumeric, Xml) wires an XmlScanner into itself in its constructor to neutralize XXE/external-entity attacks; getSecurityScannerOrThrow() enforces that invariant whenever a load path needs to scan. Hitting it means the reader instance never got a scanner — almost always a custom BaseReader subclass (or code that cleared the protected $securityScanner property).","triggerScenarios":"class MyReader extends BaseReader whose constructor never runs $this->securityScanner = XmlScanner::getInstance($this);, then ->load() hits a scan call; unserialized/cloned reader instances where the scanner property was lost; partial copies of stock reader constructors from older PhpSpreadsheet versions.","commonSituations":"Writing a custom format reader on top of BaseReader; refactoring a stock reader and dropping the constructor scanner wiring.","solutions":["In the custom reader constructor add: $this->securityScanner = XmlScanner::getInstance($this);","Or extend the closest concrete reader (Csv, Xlsx, ...) which already wires the scanner","For stock formats use IOFactory::createReader()/IOFactory::load() so a fully initialized reader is built for you"],"exampleFix":"// before\nclass MyReader extends BaseReader\n{\n    public function load(string $filename, int $flags = 0): Spreadsheet\n    {\n        $xml = $this->getSecurityScannerOrThrow()->scanFile($filename); // throws\n        // ...\n    }\n}\n\n// after\nclass MyReader extends BaseReader\n{\n    public function __construct()\n    {\n        $this->securityScanner = XmlScanner::getInstance($this);\n    }\n    // ...\n}","handlingStrategy":"type-guard","validationCode":"$reader = new MyReader();\nif ($reader->getSecurityScanner() === null) {\n    throw new RuntimeException(get_class($reader) . ' has no XmlScanner wired; fix its constructor');\n}","typeGuard":"function hasSecurityScanner(BaseReader $reader): bool\n{\n    return $reader->getSecurityScanner() instanceof XmlScanner;\n}","tryCatchPattern":"try {\n    $spreadsheet = $reader->load($file);\n} catch (ReaderException $e) {\n    if (str_contains($e->getMessage(), 'Security scanner')) {\n        // custom reader is missing scanner wiring — fix constructor, not the call site\n    }\n    throw $e;\n}","preventionTips":["In every custom BaseReader subclass, set $this->securityScanner = XmlScanner::getInstance($this) in the constructor","Prefer IOFactory::createReader() for stock formats so wiring is done for you","Add a construction smoke test asserting getSecurityScanner() is non-null for each custom reader"],"tags":["security-scanner","custom-reader","xxe","base-reader"],"backgroundTag":"null-service-dependency","analyzedSha":"65b080eef4d9fd11a5796135ab145883e5c3d6a6","analyzedAt":"2026-08-17T05:40:41.646Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}