PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Reader\Exception
Security scanner is unexpectedly null
Error message
Security scanner is unexpectedly null
What it means
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).
Source
Thrown at src/PhpSpreadsheet/Reader/BaseReader.php:241
* Create a blank sheet if none are read,
* possibly due to a typo when using LoadSheetsOnly.
*/
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): static
{
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
return $this;
}
public function getSecurityScanner(): ?XmlScanner
{
return $this->securityScanner;
}
public function getSecurityScannerOrThrow(): XmlScanner
{
if ($this->securityScanner === null) {
throw new ReaderException('Security scanner is unexpectedly null');
}
return $this->securityScanner;
}
protected function processFlags(int $flags): void
{
if (((bool) ($flags & self::LOAD_WITH_CHARTS)) === true) {
$this->setIncludeCharts(true);
}
if (((bool) ($flags & self::READ_DATA_ONLY)) === true) {
$this->setReadDataOnly(true);
}
if (((bool) ($flags & self::IGNORE_EMPTY_CELLS)) === true) {
$this->setReadEmptyCells(false);
}
if (((bool) ($flags & self::IGNORE_ROWS_WITH_NO_CELLS)) === true) {
$this->setIgnoreRowsWithNoCells(true);View on GitHub (pinned to 65b080eef4)
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
Example fix
// before
class MyReader extends BaseReader
{
public function load(string $filename, int $flags = 0): Spreadsheet
{
$xml = $this->getSecurityScannerOrThrow()->scanFile($filename); // throws
// ...
}
}
// after
class MyReader extends BaseReader
{
public function __construct()
{
$this->securityScanner = XmlScanner::getInstance($this);
}
// ...
} Defensive patterns
Strategy: type-guard
Validate before calling
$reader = new MyReader();
if ($reader->getSecurityScanner() === null) {
throw new RuntimeException(get_class($reader) . ' has no XmlScanner wired; fix its constructor');
} Type guard
function hasSecurityScanner(BaseReader $reader): bool
{
return $reader->getSecurityScanner() instanceof XmlScanner;
} Try / catch
try {
$spreadsheet = $reader->load($file);
} catch (ReaderException $e) {
if (str_contains($e->getMessage(), 'Security scanner')) {
// custom reader is missing scanner wiring — fix constructor, not the call site
}
throw $e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: Writing a custom format reader on top of BaseReader; refactoring a stock reader and dropping the constructor scanner wiring.
Related errors
- Reader classes must implement their own loadSpreadsheetFromF
- Reader classes must implement their own listWorksheetInfo()
- Failed to load file {filename} as a DOM Document
- Cloning the calculation engine is not allowed!
- Locale file not found
AI-assisted analysis of PHPOffice/PhpSpreadsheet@65b080eef4 (2026-08-17).
Data as JSON: /api/errors/900aa728840e134b.
Report an issue: GitHub.