PHPOffice/PhpSpreadsheet · error · PhpOffice\PhpSpreadsheet\Reader\Exception

Unexpected file pass record length

Error message

Unexpected file pass record length

What it means

Thrown while PhpSpreadsheet's Xls reader parses the FILEPASS record in the workbook-globals stream of a binary .xls (BIFF) file. FILEPASS marks the workbook as encrypted; the built-in RC4 decryptor expects at least 54 bytes of payload (2+2 version words, 16-byte document id, 16-byte salt, 16-byte hashed salt). A shorter record means the file uses a different encryption layout or is damaged.

Source

Thrown at src/PhpSpreadsheet/Reader/Xls.php:868

     *
     * This record is part of the File Protection Block. It
     * contains information about the read/write password of the
     * file. All record contents following this record will be
     * encrypted.
     *
     * --    "OpenOffice.org's Documentation of the Microsoft
     *         Excel File Format"
     *
     * The decryption functions and objects used from here on in
     * are based on the source of Spreadsheet-ParseExcel:
     * https://metacpan.org/release/Spreadsheet-ParseExcel
     */
    protected function readFilepass(): void
    {
        $length = self::getUInt2d($this->data, $this->pos + 2);

        if ($length < 54) {
            throw new Exception('Unexpected file pass record length');
        }

        $recordData = $this->readRecordData($this->data, $this->pos + 4, $length);

        // move stream pointer to next record
        $this->pos += 4 + $length;

        if (substr($recordData, 0, 2) !== "\x01\x00" || substr($recordData, 4, 2) !== "\x01\x00") {
            throw new Exception('Unsupported encryption algorithm');
        }
        if (!$this->verifyPassword($this->encryptionPassword, substr($recordData, 6, 16), substr($recordData, 22, 16), substr($recordData, 38, 16), $this->md5Ctxt)) {
            throw new Exception('Decryption password incorrect');
        }

        $this->encryption = self::MS_BIFF_CRYPTO_RC4;

        // Decryption required from the record after next onwards
        $this->encryptionStartPos = $this->pos + self::getUInt2d($this->data, $this->pos + 2);

View on GitHub (pinned to 65b080eef4)

Solutions

  1. Open the file in Excel or LibreOffice, remove the password (File > Info > Protect Workbook / Save As without encryption) and load it again
  2. Re-save the workbook as unencrypted .xlsx and read that instead
  3. Verify the file actually opens in Excel; if not, it is corrupt and must be re-exported or repaired
  4. Decrypt externally (e.g. LibreOffice headless convert or an MS-OFFICE-CRYPTO tool) and point the reader at the decrypted copy

Example fix

// before
$spreadsheet = IOFactory::load('legacy-protected.xls'); // Unexpected file pass record length

// after: strip the password with LibreOffice, then read the clear copy
shell_exec('soffice --headless --convert-to xlsx --outdir /tmp legacy-protected.xls');
$spreadsheet = IOFactory::load('/tmp/legacy-protected.xlsx');
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $spreadsheet = $reader->load($path);
} catch (\PhpOffice\PhpSpreadsheet\Reader\Exception $e) {
    if (str_contains($e->getMessage(), 'Unexpected file pass record length')) {
        // encrypted with an unsupported layout or corrupt: ask for a clean/re-saved file
    }
}

Prevention

When it happens

Trigger: Loading an .xls saved with legacy XOR obfuscation (its FILEPASS payload is only 4 bytes), a FILEPASS record truncated by an interrupted download/transfer, or a file emitted by a non-Microsoft writer that lays out the record incorrectly.

Common situations: Password-protected files exported from old Excel 5/95-era ERP systems, Excel '97 files re-saved by LibreOffice with XOR encryption, corrupted email attachments, or files whose bytes were mangled by a text-mode FTP transfer.

Related errors


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