Alexandro.Net

Version 1.1.0

@stackline/cfb

Drop-in replacement for cfb 1.2.2: read and write compound files (OLE/CFB), ZIP and MHT containers, with fixes for hangs, ZIP64, CVE-2024-22363 and bugs the original never released

Installation

npm install @stackline/cfb@1.1.0

To replace cfb without changing application code:

npm install cfb@npm:@stackline/cfb@1.1.0

Node.js: >=0.8. Read the compatibility notes before migrating.

Usage and API

@stackline/cfb

Drop-in replacement for cfb 1.2.2 that ends the endless loops on damaged files, fixes the CVE-2024-22363 regular expression, and reads ZIP64 archives, with identical results everywhere else. Zero dependencies; Node.js 0.8 and newer, browsers, and TypeScript.

npm version license GitHub repository Docs Reddit community

Documentation | npm | Issues | Repository

Current package version: 1.1.0


Why this package?

Change one line in package.json. Keep every result. Stop the hangs.

cfb@1.2.2 reads and writes OLE compound files (XLS, DOC, PPT, MSG, MSI), ZIP containers, and MIME aggregate documents. It was last published in April 2022. Since then SheetJS has fixed several bugs in the copy of this code embedded in xlsx, including the regular expression behind CVE-2024-22363 and ZIP64 support, but never released those fixes to the cfb package. A damaged file can still make cfb@1.2.2 loop forever.

@stackline/cfb is built for the swap:

Trust and maintenance

Compatibility

Item Value
Package @stackline/cfb@1.1.0
Replaces cfb@1.2.2 and earlier 1.x releases
Node.js runtime >=0.8
Module entry ./cfb (CommonJS)
Browser builds dist/cfb.js, dist/cfb.min.js, dist/xlscfb.js
Type declarations types/index.d.ts
Runtime dependencies None

Installation

Replace cfb without changing application code:

npm install cfb@npm:@stackline/cfb

Or install under the package's public name:

npm install @stackline/cfb

In the browser:

<script src="dist/cfb.min.js" type="text/javascript"></script>

The xlscfb.js file is designed to be embedded in js-xlsx.

Usage

const CFB = require('cfb'); // or require('@stackline/cfb')

const container = CFB.read('report.xls', { type: 'file' });
const workbook = CFB.find(container, 'Workbook');
const data = workbook.content;

Create a ZIP file:

const archive = CFB.utils.cfb_new();
CFB.utils.cfb_add(archive, 'hello.txt', Buffer.from('Hello'));
const bytes = CFB.write(archive, { type: 'buffer', fileType: 'zip' });

What the swap fixes

Input cfb@1.2.2 @stackline/cfb
A ZIP entry whose DEFLATE stream is shorter than its declared size Rest of the content is uninitialized process memory Rest is zero-filled
A FAT chain that loops (js-cfb#11) Never returns Throws Cycle detected in FAT chain at sector N
A DEFLATE stream that ends early, or has a zero-bit code Never returns Throws Unexpected end of DEFLATE stream or Invalid DEFLATE literal/length code
A MIME header line with many colons Quadratic time (CVE-2024-22363 pattern) Linear time, same matches
Directory parent links that loop RangeError: Invalid string length Throws Directory tree cycle at entry N
ZIP64 archives (sheetjs#2766) Throws Read
Stored ZIP entries with a data descriptor Throws Bad compressed size Read
An entry listed before its parent storage (js-cfb#15) Path without its storage Full path
ZIP dates read on the 29th-31st of a month Can land in the wrong month Correct date
Names of 32 or more characters written to CFB (js-cfb#18) Invalid name record 31 characters
String content written to ZIP Throws Written
A parsed container passed to read (js-cfb#20) TypeError from inside the parser TypeError that explains the mistake

See Compatibility for the full contract and how it is verified, and Upstream issues for every upstream issue, pull request, embedded xlsx fix, advisory, and fork.

Write without the seed stream

cfb@1.2.2 adds a \u0001Sh33tJ5 stream to every CFB file it writes. Set noseed to leave it out (js-cfb#22):

const bytes = CFB.write(container, { type: 'buffer', noseed: true });

Without the option, output is unchanged.

Add many entries

cfb_add rebuilds the directory after every call. For bulk additions, skip the checks with unsafe and rebuild once with cfb_gc (js-cfb#2):

const archive = CFB.utils.cfb_new();
for (const [name, content] of entries) {
  CFB.utils.cfb_add(archive, `/data/${name}`, content, { unsafe: true });
}
CFB.utils.cfb_gc(archive);

With unsafe, adding a name that already exists creates a duplicate entry.

Replace a stream

cfb_add replaces the content of an existing entry (js-cfb#21):

const container = CFB.read('report.xls', { type: 'file' });
CFB.utils.cfb_add(container, '/Workbook', updatedBytes);
CFB.writeFile(container, 'report-updated.xls');

API

TypeScript definitions are maintained in types/index.d.ts.

The CFB object exposes the following methods and properties:

CFB.parse(blob) takes a nodejs Buffer or an array of bytes and returns a parsed representation of the data.

CFB.read(blob, opts) wraps parse.

CFB.find(cfb, path) performs a case-insensitive match for the path (or file name, if there are no slashes) and returns an entry object or null if not found.

CFB.write(cfb, opts) generates a file based on the container.

CFB.writeFile(cfb, filename, opts) creates a file with the specified name.

Parse Options

CFB.read takes an options argument. opts.type controls the behavior:

type expected input
"base64" string: Base64 encoding of the file
"binary" string: binary string (byte n is data.charCodeAt(n))
"buffer" nodejs Buffer
"file" string: path of file that will be read (nodejs only)
(default) buffer or array of 8-bit unsigned int (byte n is data[n])

type: "file" needs the Node.js fs module. In browsers, read the file first and pass its bytes (js-cfb#16).

Write Options

CFB.write and CFB.writeFile take an options argument.

opts.type controls the behavior:

type output
"base64" string: Base64 encoding of the file
"binary" string: binary string (byte n is data.charCodeAt(n))
"buffer" nodejs Buffer
"file" string: path of file that will be created (nodejs only)
(default) buffer if available, array of 8-bit unsigned int otherwise

opts.fileType controls the output file type:

fileType output
'cfb' (default) CFB container
'zip' ZIP file
'mad' MIME aggregate document

opts.compression enables DEFLATE compression for ZIP file type.

opts.noseed leaves the \u0001Sh33tJ5 stream out of CFB output.

Utility Functions

The utility functions are available in the CFB.utils object. Functions that accept a name argument strictly deal with absolute file names:

By default, the library uses a pure JS inflate/deflate implementation. NodeJS zlib.InflateRaw exposes the number of bytes read in versions after 8.11.0. If a supplied zlib does not support the required features, a warning will be displayed in the console and the pure JS fallback will be used.

Container Object Description

The objects returned by parse and read have the following properties:

interface CFBEntry {
  name: string; /** Case-sensitive internal name */
  type: number; /** 1 = dir, 2 = file, 5 = root ; see [MS-CFB] 2.6.1 */
  content: Buffer | number[] | Uint8Array; /** Raw Content */
  ct?: Date; /** Creation Time */
  mt?: Date; /** Modification Time */
  ctype?: String; /** Content-Type (for MAD) */
}

Command-Line Utility

The cfb-cli module ships with a CLI tool for manipulating and inspecting supported files.

References

Security

Damaged or hostile input raises an error instead of hanging: looping FAT chains, looping directory links, and truncated or invalid DEFLATE data are detected, and the MIME header parser runs in linear time. Parsed content never includes uninitialized memory, which cfb@1.2.2 returns for a DEFLATE stream shorter than its declared size.

The parser does not cap sizes. A valid archive can still declare a large decompressed size, as with any ZIP reader. Limit input sizes and run untrusted files in a worker or process with resource limits.

See the security policy.

Local Development

Clone the repository and run the following commands from its root:

npm ci
npm test

npm test builds the CommonJS and browser files from bits/, then runs the upstream API suite, the regression tests, the Node.js 0.8-compatible smoke test, and the type checks. Source changes belong in bits/; the build regenerates cfb.js, xlscfb.js, and dist/.

Differential testing

Compare this package with cfb@1.2.2 on any directory of CFB, ZIP, and MIME files:

npm run test:differential -- <corpus-directory>
npm run test:differential -- <corpus-directory> --mutations 10 --max-size 65536

Each input runs in child processes with time and memory limits. The harness reports each input as identical, a long-name write difference, newly readable, or a regression, and exits with 1 on a regression or on a failure that only this package has. To reproduce a finding, pass its label from the report to --match and add --save <directory> to keep the corrupted input. See how compatibility is verified.

Consumer Smoke Test

npm run test:package packs the library and exercises an isolated consumer using the repository fixture.

Release Checklist

  1. Update the package version, lockfile, generated version fields, and changelog together.
  2. Run the development checks above and audit both npm audit and npm audit --omit=dev.
  3. Use the GitHub publish workflow with its Prod environment to publish the exact CI tarball.
  4. Verify public npm bytes, package identity, provenance, and the immutable GitHub release evidence.

License

Apache-2.0. Original copyright notices and upstream attribution are retained.

Please consult the attached LICENSE file for details. All rights not explicitly granted by the Apache 2.0 License are reserved by the Original Author.

@stackline/cfb is an independent project and is not affiliated with or endorsed by SheetJS LLC.

Credits and original authors

Original copyright, license notices and contributor acknowledgements remain part of this distribution. Stackline maintenance does not replace authorship of the original work.

Community and Links

Use this repository's issue tracker for reproducible bugs and feature requests. Join r/Stackline for examples, usage questions and release discussions.

Compatibility with cfb 1.2.2

Compatibility

@stackline/cfb is a drop-in replacement for cfb@1.2.2. Code that works with cfb 1.2.2 reads the same containers and writes the same bytes after the swap. Behavior changes only where cfb@1.2.2 never returns, cannot read a valid file, or produces an invalid or wrong result.

npm install cfb@npm:@stackline/cfb

Contract

How compatibility is verified

scripts/differential.mjs runs cfb@1.2.2 and this package side by side, each input in its own child process with time and memory limits, and compares every observable result: the read container, find, CFB output, ZIP output with and without compression, the container after each write, and a write-read round trip. With --mutations N it also corrupts each small input N times with a fixed seed.

npm run test:differential -- <corpus-directory>

Each input is reported in one class:

Class Meaning Fails the run
identical Every result matches No
long-name Only CFB writes differ, and only because a name of 32 or more characters is written as 31; the round trip matches except that each such name reads back one or two characters shorter No
local-accepts cfb@1.2.2 throws and this package returns a result No
upstream-failure-fixed cfb@1.2.2 times out, runs out of memory, or crashes, and this package finishes No
error-text Both throw, with different messages No
both-failed Both time out or run out of memory No; listed in the output
local-failure Only this package times out, runs out of memory, or crashes Yes
regression cfb@1.2.2 returns a result and this package returns a different one or throws Yes

CI runs the harness on the repository fixtures. Results for release 1.1.0, against the SheetJS test corpus (1,284 compound files and 1,674 ZIP containers):

Run Inputs Result
Files up to 16 MB, all operations 2,951 2,924 identical; 24 long-name; 2 encrypted ODS files that cfb@1.2.2 cannot read now read (local-accepts); 1 both-failed (memory limit of the test)
Files above 8 MB, reading only 9 8 identical; 1 both-failed (memory limit of the test)
Corrupted copies of the files up to 64 KB, 10 each (seed 24301, 5-second limit) 29,095 No input failed only in this package; details below

The corruption run:

Class Inputs What it is
identical 25,194
upstream-failure-fixed 1,442 cfb@1.2.2 hangs or exceeds the memory limit; this package throws, mostly Unexpected end of DEFLATE stream
error-text 2,411 Both throw, with different messages; 2,190 differ only in the numbers of a checksum or size error
long-name 19
local-accepts 19
both-failed 3 Both exceed the 700 MB memory limit of the test
regression 7 5 entries where cfb@1.2.2 returned uninitialized memory, and 2 entries that cfb@1.2.2 leaves without a parent; both are intentional differences

cfb@1.2.2 also varies from run to run on damaged input: it returns uninitialized memory, and its Huffman tables keep entries from earlier streams. Replayed one at a time against cfb@1.2.2 with zero-filled buffers, the 5 memory cases are identical, and of 20 error-text inputs sampled evenly, 17 are identical, 2 hang in cfb@1.2.2, and 1 decodes about 4 billion bits past the end of its data in cfb@1.2.2 before failing its checksum.

test/fixes.test.cjs covers each intentional difference with a reproduction, and asserts what cfb@1.2.2 does where it finishes.

Intentional differences

Input cfb@1.2.2 @stackline/cfb
A ZIP entry whose DEFLATE stream is shorter than its declared size, with a data descriptor The rest of the content is uninitialized memory, which can hold data left by earlier work in the process and differ between runs The rest is zero-filled
A FAT chain that loops (js-cfb#11) Never returns Cycle detected in FAT chain at sector N
A DEFLATE stream that ends before its final block Keeps decoding zero bits past the end: never returns, or after about 4 billion bits its 32-bit bit offset wraps around and it decodes the data again from the start Unexpected end of DEFLATE stream
A DEFLATE code of zero bits (invalid Huffman table) Never returns; memory passed 2 GB within 10 seconds in testing Invalid DEFLATE literal/length code
A MIME header line with many colons Quadratic time (CVE-2024-22363 pattern) Linear time, same matches
Directory parent links that loop RangeError: Invalid string length Directory tree cycle at entry N
An entry that cfb@1.2.2 leaves without a parent: listed before its parent storage (js-cfb#15), or a sibling of a stream whose child link points at itself Path without its storage or the root Full path from the directory tree
ZIP64 archives Throws Read
Stored ZIP entries with a data descriptor Bad compressed size Read
ZIP dates read on the 29th-31st of a month Month can shift by one Correct date
Names of 32 or more characters, written to CFB 32 characters with a 66-byte length field; reads back with a corrupt 33rd character and is rejected by strict readers 31 characters, as on upstream master (js-cfb#18)
String content written to ZIP Throws Written
A parsed container passed to read (js-cfb#20) TypeError: file.slice is not a function or similar TypeError explaining the mistake

console.error reports a truncated name in both versions; the message now names the 31-character result.

Opt-in additions

Option Default Purpose
CFB.write(cfb, {noseed: true}) false Write CFB output without the \u0001Sh33tJ5 seed stream (js-cfb#22)

The type declarations also describe utils.use_zlib, utils._deflateRaw, utils._inflateRaw, and utils.consts, which cfb@1.2.2 exports without types.

See UPSTREAM.md for every upstream issue, pull request, embedded xlsx fix, advisory, and fork.

Upstream issues, pull requests, and forks

Upstream issues, pull requests, and forks

This page maps the original project and everything reported against it to how @stackline/cfb handles each item. It was last reviewed on 2026-10-13.

The rule for every decision: results for existing cfb@1.2.2 code do not change. A fix becomes the default only when cfb@1.2.2 never returns, fails on a valid file, or produces a wrong or invalid result. Everything else is opt-in or declined.

Status Meaning
Fixed Works without configuration
Opt-in Available through an option; the default is unchanged
Supported Already worked in cfb@1.2.2 and still does
Declined Would change cfb@1.2.2 results or break the format; the reason is given

Where the original stands

Issues (SheetJS/js-cfb)

Issue Problem Status How
#1 Cannot find name 'Buffer' in the types Supported Fixed in cfb 1.0.4
#2 cfb_add rebuilds the container on every call Supported cfb_add(..., {unsafe: true}) then one cfb_gc; documented in the README
#3 MSI entry names look garbled Supported MSI compresses names; not a container bug
#4 How to decompress VBA streams Declined MS-OVBA is outside the container format
#5 Missing root prefix when a child precedes its parent Supported Fixed upstream (PR #6)
#7 Endless loop in build_full_paths Fixed A cycle of parent links now raises Directory tree cycle at entry N; cfb@1.2.2 raises RangeError: Invalid string length
#8 Endless loop in make_sector_list Supported Fixed in cfb 1.1.4
#9 Memory use on large XLS files Supported Closed upstream; large corpus files read the same as in cfb 1.2.2
#11 Endless loop in get_sector_list on a damaged file Fixed Cyclic FAT chains raise Cycle detected in FAT chain at sector N
#12 Quadratic rebuild_cfb Supported Fixed in cfb 1.2.1
#14 DIFAT recursion past its count Supported Bounded by the count in cfb 1.2.2
#15 An entry listed before its parent storage loses its path Fixed Entries left without a parent take it from the directory tree; no other path changes
#16 fs.readFileSync is not a function in browsers Supported type: "file" is Node-only; documented
#18 Names must fit 31 characters plus a terminator Fixed Names are truncated to 31 characters, as on upstream master
#20 file.slice is not a function Fixed Passing a parsed container to read raises a TypeError that says so
#21 How to update a stream inside a file Supported cfb_add replaces existing content; documented
#22 Write without the \u0001Sh33tJ5 seed stream Opt-in CFB.write(cfb, {noseed: true})
#24 Keep stream order instead of sorting Declined [MS-CFB] 2.6.4 requires sibling order for binary search; unsorted files fail lookups in other readers

Pull requests (SheetJS/js-cfb)

Pull request Change Status
#6 Fix for #5 Supported
#10 Move the CLI to cfb-cli Supported
#13 Buffer.copy on write Supported
#17 Remove the unused printj dependency Supported
#19 Truncate names to 31 characters Fixed

Fixes from the copy embedded in xlsx

xlsx change Problem in cfb@1.2.2 Status
a0bed2a9 (CVE-2024-22363) MIME header regular expression backtracks quadratically Fixed: /^([^:]*?):\s*([^\s].*)$/ matches the same lines in linear time
045adba8 (sheetjs#2766) ZIP64 archives cannot be read Fixed, following APPNOTE 4.5.3: ZIP64 end records, central directory fields, local fields, and 8-byte data descriptors
5a36cb42 Stored entries with a data descriptor are misread Fixed; streamed stored entries also take their size from the central directory
81b231d8 String content breaks ZIP output Fixed
16dd8a6e CRC tables assume Int32Array#subarray Fixed
050f66ce Message when the directory start is out of range Declined: both versions throw; only the message would change
2022f7f4 require('fs') replaced by an injected module Declined: changes how Node loads the module

Security advisories

Problems found during this review

Problem in cfb@1.2.2 Status
inflate returns uninitialized process memory after the end of a stream that is shorter than its declared size; through read, a ZIP entry with a data descriptor carries that memory as content, because the CRC mismatch is only a warning Fixed: the rest of the output is zero-filled
Huffman tables are shared between calls and never cleared, so damaged DEFLATE data that reaches an unused bit pattern decodes with entries left by an earlier block or stream; the result can depend on what the process decoded before Declined: those leftover entries let cfb@1.2.2 read some damaged files. Clearing them changed 12 results in the corruption run, 10 of them from content into errors. Valid streams never reach unused patterns
A stream whose child link points at itself leaves its siblings without a parent, so their paths lose the root Fixed, with js-cfb#15
inflate never returns on a stream that ends before its final block (a CPU loop over zero bits past the end, which can end only when its 32-bit bit offset wraps around after about 4 billion bits), or on a code of zero bits (memory passed 2 GB within 10 seconds) Fixed: Unexpected end of DEFLATE stream or Invalid DEFLATE literal/length code
ZIP dates read on the 29th-31st of a month can land in the wrong month, because fields were set one by one on today's date Fixed
Names of 32 or more characters were written with a 66-byte length field; the file reads back with a corrupt name and strict readers such as olefile reject it Fixed (31-character limit)
CFB.version reported 1.0.2 in @stackline/cfb 1.0.4 Fixed: the build writes the package version
engines required Node.js 20.19 although the code runs on 0.8 Fixed: >=0.8, tested from 0.8.28 to 24

Downstream reports in SheetJS/sheetjs about this code

Report Status
#2766 ZIP64 workbooks Fixed
#3215, #3244 endless CPU and memory on corrupt archives The endless loops in inflate are fixed; the samples attached to these reports already fail quickly in standalone cfb@1.2.2
#2745, #2965, #3274 "Bad compressed size" with data descriptors Fixed for stored entries, including two encrypted ODS files in the test corpus; other cases were not reproduced
#569, #1832, #1872 crashes in make_sector_list Supported (guarded in cfb 1.2.2)
#1419, #1442 CLSID check on WPS files Supported (cfb 1.1.1)

Forks

All 16 GitHub forks of SheetJS/js-cfb were compared with upstream master. Two carry their own code:

Fork Change Status
albanm/js-cfb (@albanm/cfb) FAT cycle guard in make_sector_list Supported: upstream added a stronger guard in 1.1.4
YuryAlkevich/js-cfb Recursive directory add in the CLI Declined: CLI only, not library behavior

isabella232/js-cfb could not be inspected (the API returns 404). The others have no commits of their own.

Release changes

Changelog

1.1.0 - 2026-10-13

A drop-in replacement for cfb@1.2.2. On the SheetJS test corpus every read result is identical, and written bytes differ only where cfb@1.2.2 wrote invalid names. On damaged input this release finishes where cfb@1.2.2 never returns, and it never returns uninitialized memory. See docs/COMPATIBILITY.md and docs/UPSTREAM.md.

Fixed

Added

1.0.4 - 2026-09-28

1.0.3 - 2026-09-28

1.0.2 - 2026-09-28

1.0.1 (2026-09-28)

1.0.0

Upstream history

CHANGELOG

This log is intended to keep track of backwards-incompatible changes, including but not limited to API changes and file location changes. Minor behavioral changes may not be included if they are not expected to break existing code.

1.2.1 (2021-09-06)

1.2.0 (2020-07-09)

1.1.0 (2018-09-04)

1.0.6 (2018-04-09)

1.0.0 (2017-11-05)

0.14.0 (2017-11-04)

Release files and references

Package bytes, npm provenance and the immutable GitHub release were verified for this version. Security checks describe the reviewed release; documented compatibility risks and upstream reports are not blanket claims of resolution.