{"_id":"exceljs-hardened","name":"exceljs-hardened","dist-tags":{"latest":"5.0.0"},"versions":{"5.0.0":{"name":"exceljs-hardened","version":"5.0.0","description":"Unofficial security-hardened fork of exceljs, patching known unpatched vulnerabilities in the upstream (unmaintained) project. Not affiliated with the original exceljs maintainers.","private":false,"license":"MIT","author":{"name":"mateo@callec.net"},"contributors":[{"name":"Guyon Roche","email":"guyon@live.com"}],"repository":{"type":"git","url":"git+https://github.com/mateocallec/exceljs-hardened.git"},"bugs":{"url":"https://github.com/mateocallec/exceljs-hardened/issues"},"homepage":"https://github.com/mateocallec/exceljs-hardened#readme","engines":{"node":">=8.3.0"},"main":"./excel.js","browser":"./dist/exceljs.min.js","types":"./index.d.ts","scripts":{"test":"npm run test:full","test:es5":"export EXCEL_BUILD=es5 && npm run test:full","test:full":"npm run build && npm run test:unit && npm run test:integration && npm run test:end-to-end && npm run test:jasmine","test:version":"npm run build && npm run test:unit && npm run test:integration && npm run test:end-to-end && npm run test:browser && npm run test:dist","test:all":"npm run test:native && npm run test:es5","test:native":"npm run test:full","test:unit":"mocha --require spec/config/setup --require spec/config/setup-unit spec/unit --recursive","test:integration":"mocha --require spec/config/setup spec/integration --recursive","test:end-to-end":"mocha --require spec/config/setup spec/end-to-end --recursive","test:browser":"if [ ! -f .disable-test-browser ]; then npm run build && npm run test:jasmine; fi","test:jasmine":"grunt jasmine","test:unit:es5":"export EXCEL_BUILD=es5 && npm run test:unit","test:integration:es5":"export EXCEL_BUILD=es5 && npm run test:integration","test:end-to-end:es5":"export EXCEL_BUILD=es5 && npm run test:end-to-end","test:dist":"mocha --require spec/config/setup spec/dist --recursive","test:manual":"node spec/manual/app.js","test:typescript":"mocha -r ts-node/register spec/typescript/**/*.spec.ts","clean-build":"npm run clean && npm run build","lint":"eslint --format node_modules/eslint-friendly-formatter .","lint:fix":"prettier-eslint --write $(pwd)'/**/*.js'","lint:staged":"lint-staged","clean":"rm -rf build/ && rm -rf dist","benchmark":"node --expose-gc benchmark","benchmark:debug":"node --expose-gc --inspect-brk --trace-deopt benchmark","build":"grunt build","install-build":"npm install && grunt build","preversion":"npm run clean && npm run build && npm run test:version","postversion":"git push --no-verify && git push --tags --no-verify"},"husky":{"hooks":{"pre-commit":"lint-staged"}},"lint-staged":{"*.js":["prettier-eslint --write","eslint --format node_modules/eslint-friendly-formatter","git add"]},"keywords":["xlsx","json","csv","excel","font","border","fill","number","format","number format","alignment","office","spreadsheet","workbook","defined names","data validations","rich text","in-cell format","outlineLevel","views","frozen","split","pageSetup","security","hardened","patched"],"dependencies":{"archiver":"^5.0.0","dayjs":"^1.8.34","fast-csv":"^4.3.1","jszip":"^3.10.1","readable-stream":"^3.6.0","saxes":"^5.0.1","tmp":"^0.2.0","unzipper":"^0.10.11","uuid":"^8.3.0"},"devDependencies":{"@babel/cli":"^7.10.5","@babel/core":"^7.11.4","@babel/preset-env":"^7.11.0","@types/chai":"^4.2.12","@types/mocha":"^8.0.3","@types/node":"^14.11.2","babelify":"^10.0.0","browserify":"^16.5.2","chai":"^4.2.0","chai-datetime":"^1.7.0","chai-xml":"^0.3.2","core-js":"^3.6.5","dirty-chai":"^2.0.1","eslint":"^6.5.1","eslint-config-airbnb-base":"^14.2.0","eslint-config-prettier":"^6.12.0","eslint-friendly-formatter":"^4.0.1","eslint-plugin-import":"^2.22.0","eslint-plugin-node":"^11.1.0","express":"^4.16.4","got":"^9.0.0","grunt":"^1.3.0","grunt-babel":"^8.0.0","grunt-browserify":"^5.3.0","grunt-contrib-copy":"^1.0.0","grunt-contrib-jasmine":"^2.2.0","grunt-contrib-watch":"^1.1.0","grunt-exorcise":"^2.1.1","grunt-terser":"^1.0.0","husky":"^4.3.0","lint-staged":"^10.2.13","mocha":"^7.2.0","prettier-eslint":"^11.0.0","prettier-eslint-cli":"^5.0.0","regenerator-runtime":"^0.13.7","sax":"^1.2.4","ts-node":"^8.10.2","typescript":"^3.9.7"},"gitHead":"37149551343c6305aa8aca4f43567c1091102287","_id":"exceljs-hardened@5.0.0","_nodeVersion":"24.19.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-qRWOd+qudmL+PYN50jktguyngu4cDNWmvJTd2YeGNwgLJy+1+dmAQIVS0ZOF5YphefzgW4288gjIvJn1ZkUZTQ==","shasum":"262723b35941dfde21f3ab5f1ecdcfebfe9bbf53","tarball":"https://registry.npmjs.org/exceljs-hardened/-/exceljs-hardened-5.0.0.tgz","fileCount":888,"unpackedSize":25446194,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIE2SpaItvkxtSUUmlHGC29UND+BbN7fR4sbgC+ZY74zUAiEA9N5NSQOeBwDguuGj8XxYWo3TfxUq4YDhau5BwHpYWVk="}]},"_npmUser":{"name":"mateocallec","email":"mateo@callec.net"},"directories":{},"maintainers":[{"name":"mateocallec","email":"mateo@callec.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/exceljs-hardened_5.0.0_1787448578599_0.48228337250770426"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-23T01:29:38.530Z","5.0.0":"2026-08-23T01:29:38.850Z","modified":"2026-08-23T01:29:39.074Z"},"maintainers":[{"name":"mateocallec","email":"mateo@callec.net"}],"description":"Unofficial security-hardened fork of exceljs, patching known unpatched vulnerabilities in the upstream (unmaintained) project. Not affiliated with the original exceljs maintainers.","homepage":"https://github.com/mateocallec/exceljs-hardened#readme","keywords":["xlsx","json","csv","excel","font","border","fill","number","format","number format","alignment","office","spreadsheet","workbook","defined names","data validations","rich text","in-cell format","outlineLevel","views","frozen","split","pageSetup","security","hardened","patched"],"repository":{"type":"git","url":"git+https://github.com/mateocallec/exceljs-hardened.git"},"contributors":[{"name":"Guyon Roche","email":"guyon@live.com"}],"author":{"name":"mateo@callec.net"},"bugs":{"url":"https://github.com/mateocallec/exceljs-hardened/issues"},"license":"MIT","readme":"# exceljs-hardened\n\n> [!WARNING]\n> **This is an unofficial, unaffiliated fork of [exceljs](https://github.com/exceljs/exceljs).**\n> It exists to maintain the security of a widely-used library whose upstream\n> appears unmaintained: no response channel currently works (Discord is\n> gone, GitHub private vulnerability reporting is disabled on the upstream\n> repo), and several real vulnerabilities have gone unpatched as a result.\n>\n> This fork is **not** endorsed by, and has no relationship with, the\n> original exceljs maintainers. It is not a general-purpose continuation of\n> the project — it exists specifically to carry security fixes for\n> known, reported issues that upstream cannot currently receive.\n\n## Why this exists\n\n`exceljs` is depended on by a large number of Node.js applications for\nreading/writing `.xlsx`/`.csv` files. Several vulnerabilities in the\nupstream code have real, demonstrated impact (prototype pollution,\narbitrary local file read, unbounded decompression) and no way to be\nresponsibly disclosed to, or fixed by, the original project. Rather than\nlet users choose between \"stay vulnerable\" and \"stop using the library,\"\nthis fork patches those specific issues and republishes under a\ndifferent package name so they don't collide with the official one on npm.\n\n## What's patched\n\n| ID | Vulnerability | Fixed in | Status |\n|---|---|---|---|\n| VULN-01 | Prototype pollution in `_.deepMerge()` (`lib/utils/under-dash.js`) | `5.0.0` | ✅ Patched |\n| VULN-02 | Arbitrary local file read via `addImage({filename})` (`lib/doc/workbook.js`, `lib/xlsx/xlsx.js`) | `5.0.0` | ✅ Patched |\n| VULN-03 | CSV / formula injection in `CSV.write()` (`lib/csv/csv.js`) | `5.0.0` | ✅ Patched |\n| VULN-04 | Decompression bomb / memory exhaustion in `Workbook#load()` (`lib/xlsx/xlsx.js`) | `5.0.0` | ✅ Patched |\n\nAll four issues identified in the original audit are now patched and\nverified against live proof-of-concept exploits (see\n[`../../poc/exceljs`](../../poc/exceljs) — the same PoC tools that\ndemonstrated each vulnerability were re-run against a demo app wired to\nthis fork to confirm the fixes hold).\n\nSee [`SECURITY.md`](./SECURITY.md) for how to report new issues, and each\npatch's inline `[exceljs-hardened]` comments in the source for the\ntechnical rationale.\n\n### VULN-01 — Prototype pollution\n\n`_.deepMerge()` now refuses to assign into `__proto__`, `constructor`, or\n`prototype` keys, closing the path from a JSON-parsed `cell.note` object\nto the global `Object.prototype`.\n\n### VULN-02 — Arbitrary local file read\n\n`addImage({filename})` and the internal media writer reject any raw path\ncontaining a `..` segment (`assertSafeMediaPath`) — but **that alone does\nnot fix the realistic exploit chain**, and testing against a live target\nconfirmed it: the typical vulnerable pattern is\n\n```js\nconst resolvedPath = path.join(baseDir, userInput);   // e.g. userInput = '../../../.env'\nworkbook.addImage({filename: resolvedPath});\n```\n\n`path.join()` already resolves and strips `..` segments *before*\n`addImage()` ever sees the string (`path.join('/app/assets/logos',\n'../../../.env')` → `'/app/.env'`, with no `..` left in it) — so a check\nperformed after the join has nothing left to catch.\n\n**The actual fix is `ExcelJS.utils.safeJoin(baseDir, userInput)`**, which\nresolves the path and checks containment as a single operation instead of\ntwo separate steps:\n\n```js\nconst ExcelJS = require('exceljs-hardened');\n\n// throws: Refusing to resolve \"../../../.env\" against base directory\n// \"/app/assets/logos\": it resolves to \"/app/.env\", which is outside the\n// base directory.\nconst safePath = ExcelJS.utils.safeJoin('/app/assets/logos', '../../../.env');\n```\n\n**If your application builds an `addImage({filename})` path from user\ninput, you must use `safeJoin()` instead of `path.join()`** — this is not\noptional defense in depth, it's the fix. See the demo app's\n[`../../poc/exceljs/webapp-hardened/server.js`](../../poc/exceljs/webapp-hardened/server.js)\nfor the before/after, and\n[`lib/utils/media-path-guard.js`](./lib/utils/media-path-guard.js) for\nthe full rationale (including why the naive `assertSafeMediaPath`-only\nversion of this patch shipped in an earlier draft and failed against a\nlive PoC before this fix was added).\n\n### VULN-04 — Decompression bomb\n\n`Workbook#load()` now reads each zip entry's *declared* uncompressed\nsize from the archive's central directory before decompressing it, and\nrefuses to proceed if a single entry (default cap: 128MB) or the archive\nas a whole (default cap: 512MB) would exceed a sane limit — without ever\nmaterializing the bomb in memory.\n\n```js\n// throws: Refusing to decompress \"xl/worksheets/sheet1.xml\": declared\n// uncompressed size (1610612736 bytes) exceeds the per-entry limit ...\nawait workbook.xlsx.load(maliciousBuffer);\n\n// Both limits are configurable if your legitimate files are larger:\nawait workbook.xlsx.load(buffer, {\n  maxEntryUncompressedSize: 256 * 1024 * 1024,\n  maxTotalUncompressedSize: 1024 * 1024 * 1024,\n});\n```\n\n### VULN-03 — CSV / formula injection\n\n`CSV.write()` now prefixes any value beginning with `=`, `+`, `-`, `@`,\ntab, or CR with a leading apostrophe before handing it to `fast-csv`.\nEvery mainstream spreadsheet application treats a leading `'` as \"force\nthis cell to be text\" — the apostrophe itself isn't shown, so the cell\nstill displays exactly as written, it just never gets evaluated as a\nformula.\n\n```js\nconst rows = [['Item', '=cmd|\"/c calc\"!A0']];\nrows.forEach(r => sheet.addRow(r));\nawait workbook.csv.writeBuffer();\n// -> Item,'=cmd|\"/c calc\"!A0\n//    (the payload is now inert text, not a live formula)\n```\n\nThis is applied to the *default* mapper and to any custom `options.map`\nyou supply — the wrapping happens after your mapper runs, so it protects\ncustom export logic too. If you need the raw, unescaped behavior (e.g. a\npurely numeric export you've already validated), opt out explicitly:\n\n```js\nawait workbook.csv.writeBuffer({escapeFormulas: false});\n```\n\n## Installing\n\n```bash\nnpm install exceljs-hardened\n```\n\nThe API is otherwise unchanged from upstream `exceljs@4.4.0` — this is a\ndrop-in replacement:\n\n```diff\n- const ExcelJS = require('exceljs');\n+ const ExcelJS = require('exceljs-hardened');\n```\n\n## What this fork does *not* do\n\n- It does not track every upstream change — it starts from `exceljs@4.4.0`\n  plus the specific security patches above. If upstream ever becomes\n  active again, migrating back to the official package is recommended.\n- It makes no claim of a full, independent security audit beyond the\n  four issues listed above.\n\n## Upgrading from `4.x`\n\n`5.0.0` deliberately breaks a few edge cases that upstream `4.4.0` used\nto allow silently, because those edge cases were the exploitable\nbehavior:\n\n- `addImage({filename})` now throws on a raw path containing `..`, and\n  `Workbook.utils.safeJoin()` (new) is the supported way to build such a\n  path from user input — see the VULN-02 section above.\n- `Workbook#load()` now throws by default on any zip entry declaring\n  more than 128MB uncompressed, or an archive totalling more than 512MB\n  — both configurable via `options.maxEntryUncompressedSize` /\n  `options.maxTotalUncompressedSize` if your legitimate files are larger.\n- `CSV.write()` now prefixes formula-trigger characters with `'` by\n  default — opt out per-call with `{escapeFormulas: false}` if you rely\n  on the raw output.\n\nIf you hit one of these in an existing app, it means the input in\nquestion matches the exact shape that was exploitable — that's the\npoint. Adjust the input, raise the relevant limit deliberately, or opt\nout for that call site if you've already validated it's safe.\n\n## License\n\nMIT, same as upstream — see [`LICENSE`](./LICENSE). Original copyright\n(c) 2014-2019 Guyon Roche is preserved as required by the license; the\nsecurity patches in this fork are additional contributions on top of\nthat original work.\n","readmeFilename":"README.md","_rev":"1-1314ce634d3ed96b2a039dd2de8122b7"}