Back to Blog

Security

From xlsx to exceljs: when the advisory says "no fix available"

The xlsx package on npm has known high-severity vulnerabilities. The Dependabot fix says "no fix available." The reason is structural, and the right move is to switch packages. Here is the migration I shipped, what broke, and what I learned about supply-chain hygiene.

S
Sarma
4 May 20267 min read
ShareLinkedInX

In May 2026 the security tab on Sarmalink-AI showed three high-severity Dependabot alerts for xlsx (the SheetJS package). All three came back with the same depressing message:

No fix available.

That is unusual. Most npm advisories list a patched version. The xlsx ones list a range that simply does not exist on npm. The reason is structural and worth understanding.

Why xlsx has no fix on npm

SheetJS, the company behind xlsx, removed their newer releases from the public npm registry in 2023. Their position is that the npm version is a community-maintained legacy build, and that the actively-maintained build lives only on the SheetJS CDN at cdn.sheetjs.com[3]. The legacy version on npm is 0.18.5 from 2022. Advisories list fixes in 0.19.3+ and 0.20.2+, neither of which is on npm.

This is a maintainer's choice, not a bug, but it leaves anyone using xlsx from npm with three unappealing options:

  1. Install from the SheetJS CDN directly. Means a non-standard install in package.json, breaks naive deploy pipelines, and ties you to one vendor's CDN uptime.
  2. Pin and ignore. Mark the advisory as "won't fix" in your security tab. Works for trusted internal use; risky if user input ever reaches the parser.
  3. Switch packages. Replace xlsx with a maintained alternative such as exceljs[4].

For Sarmalink-AI specifically, user input absolutely reaches the parser. Uploaded .xlsx attachments get extracted server-side as part of the file-attachment flow. Prototype pollution[1] and ReDoS[2] in that path would let any user inject arbitrary keys into Object.prototype or stall the worker. Path 3 was the only safe call.

The before-and-after

The old code, in two places (lib/file-extract.ts and lib/services/attachment-extractor.ts):

ts
async function extractExcel(base64) { const XLSX = require('xlsx') const buffer = Buffer.from(base64, 'base64') const workbook = XLSX.read(buffer, { type: 'buffer' }) let result = '' for (const sheetName of workbook.SheetNames) { const sheet = workbook.Sheets[sheetName] const csv = XLSX.utils.sheet_to_csv(sheet) result += `Sheet: ${sheetName}\n${csv}\n\n` } return result }

The new code with exceljs:

ts
async function extractExcel(base64) { const ExcelJS = require('exceljs') const buffer = Buffer.from(base64, 'base64') const workbook = new ExcelJS.Workbook() await workbook.xlsx.load(buffer) let result = '' workbook.eachSheet((sheet) => { result += `Sheet: ${sheet.name}\n` sheet.eachRow({ includeEmpty: false }, (row) => { const values = row.values.slice(1).map((v) => { if (v == null) return '' if (typeof v === 'object' && 'text' in v) return String(v.text) if (typeof v === 'object' && 'result' in v) return String(v.result) return String(v).replace(/"/g, '""') }) result += values.map((v) => /[",\n]/.test(v) ? `"${v}"` : v).join(',') + '\n' }) result += '\n' }) return result }

Two API differences worth noting.

First, exceljs is async. workbook.xlsx.load(buffer) returns a Promise where xlsx's XLSX.read(buffer) was synchronous. If you are wiring this into a route handler that already awaits other work, no problem. If you are calling from a sync codepath, you will need to thread async up the stack.

Second, exceljs does not give you "sheet to csv" out of the box. The sheet iteration model gives you cells, not pre-formatted strings. For my use case (chat-attachment extraction, where the result feeds into an LLM context), I needed to hand-roll the CSV serialisation to handle cell value types: plain values, formula results (via cell.result), and rich-text objects (via cell.text). Twelve lines of code, but worth knowing if you are migrating naively.

The cost of switching

xlsx vs exceljs: install size and posture

Source: bundlephobia + manual measurement

exceljs adds about 680KB gzipped to your node_modules versus xlsx's 750KB, mostly because it ships ZIP-stream-handling and XML-parsing code that xlsx outsourced. For a serverless function it adds roughly 20-30ms cold-start overhead. Neither hurts production for me, but if you run on the absolute edge with a tight bundle budget, profile first.

The bigger cost is exceljs's transitive dependencies. When I migrated, exceljs pulled in a uuid version with a moderate Dependabot alert. So I traded three high-severity vulns in xlsx for one moderate vuln in uuid. Net win, but not a clean replacement.

What I would do differently

Two things.

Pin to a known-clean version, not the floating latest. I pinned to whatever exceljs npm install returned, which was the latest minor. A safer move is to check the advisories on the candidate replacement first. Five minutes of audit before the swap saves a moderate alert in your security tab afterwards.

Watch for "no fix available" advisories specifically. They are the highest-priority security alerts your tooling will surface. The default npm audit fix ignores them by design, because there is nothing to fix. If you do not actively scan for them, they accumulate quietly in the Dependabot tab.

The wider lesson

Pinning a major version of an npm dependency is not enough to keep you safe. The package itself can be retired by its maintainer with no path forward. Supply-chain hygiene means knowing which of your dependencies are in active maintenance, which are in coast mode, and which have been quietly abandoned. That information is rarely on the package's npm page. It lives in GitHub commits, advisory threads, and maintainer announcements.

For the specific case of xlsx, I now check npm view xlsx versions --json | tail whenever I see it in a new project. If the latest npm version is from 2022 and the maintainer's website hosts something newer, that is a signal to reach for exceljs from the start.

The Sarmalink-AI commit that shipped this migration is 295f9c8[5] if you want to read the full diff.

---

A note on this post

The numbers, code excerpts, and commit hashes referenced are real and verifiable in the linked repository. Charts are rendered from the data tables shown above each chart. Where the post draws on third-party advisories or maintainer announcements, citations link to the primary source.

References

  1. [1]

    GHSA-4r6h-8v6p-xvw6: Prototype Pollution in sheetJS

    https://github.com/advisories/GHSA-4r6h-8v6p-xvw6
  2. [2]

    GHSA-5pgg-2g8v-p4x9: SheetJS Regular Expression Denial of Service (ReDoS)

    https://github.com/advisories/GHSA-5pgg-2g8v-p4x9
  3. [3]

    SheetJS CDN — official maintained release channel

    https://cdn.sheetjs.com
  4. [4]
  5. [5]

    Sarmalink-ai commit 295f9c8: replace abandoned xlsx package with maintained exceljs

    https://github.com/sarmakska/Sarmalink-ai/commit/295f9c8
S

Sarma

Independent software engineer — AI systems, automation platforms, and modern infrastructure.

More in Security

Work with Sarma

Have a project in mind?

I take on a small number of projects each quarter — AI systems, automation, infrastructure, and full-stack engineering.

Get in touch