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

  1. In the custom reader constructor add: $this->securityScanner = XmlScanner::getInstance($this);
  2. Or extend the closest concrete reader (Csv, Xlsx, ...) which already wires the scanner
  3. 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

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


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