{"id":"6a5676daf78fad31cc0eb55c","title":"2026 07 14 Issue 315","path":"how-to/committee/release-notes/2026-07-14-issue-315","contentMarkdown":"# 14-Jul-2026 — replace SheetJS xlsx with exceljs [ref #315](https://github.com/nbarrett/ngx-ramblers/issues/315)\n\n## [build 776](https://github.com/nbarrett/ngx-ramblers/actions/runs/29354643657) — [commit 3dda3b9](https://github.com/nbarrett/ngx-ramblers/commit/3dda3b9f4c5171722c035354e80aedf1dc89f505)\n\n_____\n\nThe Docker build for the worker image failed at `npm ci --ignore-scripts` with a\n403 from cdn.sheetjs.com. SheetJS distributes off the npm registry via their own\nCDN, and that CDN blocks datacentre IP ranges, so the tarball URL dependency can\nbe fetched from a developer machine but not from a GitHub Actions runner.\n\nRe-pinning to the registry was not a way out. xlsx@0.18.5 is the last version\nSheetJS published to npm and carries two unpatched high-severity advisories that\nnpm audit reports as fixAvailable: false, because there is nothing newer on the\nregistry to upgrade to:\n\n- prototype pollution  GHSA-4r6h-8v6p-xvw6  (< 0.19.3)\n- ReDoS                GHSA-5pgg-2g8v-p4x9  (< 0.20.2)\n\nThe prototype pollution advisory is reachable: member-bulk-load parses a workbook\nthe user has just uploaded. So the library is replaced rather than re-pinned.\n\nexceljs is npm-hosted, maintained and free of known advisories. It was the single\nconsumer in the codebase, at one call site.\n\nTo show the swap is behaviour-neutral, regression tests were written against the\nexisting xlsx reader and confirmed green first, then re-run unchanged against\nexceljs. That caught four cell types which sheet_to_json silently flattens but\nexceljs returns as objects, and which a direct swap would have written into member\nrecords as [object Object]:\n\n- auto-hyperlinked email  ->  {text, hyperlink}\n- formula                 ->  {formula, result}\n- rich text               ->  {richText: [...]}\n- date                    ->  Date, where xlsx gave an Excel serial\n\nExcel auto-hyperlinks email addresses, so the first of those was a live risk on a\nreal Ramblers export. The new reader normalises all four, and a test asserts no raw\ncell object can reach a row value.\n\nOne divergence cannot be removed: exceljs collapses an empty-string cell to null, so\nthe empty-string versus absent-key distinction is lost. Rather than assume that was\nharmless, the row-to-member mapping was extracted and tested directly: every field\npasses through trim(), and trim(undefined) === trim(\"\") === \"\", so the member records\nare identical either way.\n\nExcel date cells are still read as serial numbers, as they were before. Real Ramblers\nexports store dates as strings, so this has never been hit. Preserved bug-for-bug so a\nsecurity fix does not also change data handling.","contentHtml":"<h1>14-Jul-2026 — replace SheetJS xlsx with exceljs <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/315\">ref #315</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/29354643657\">build 776</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/3dda3b9f4c5171722c035354e80aedf1dc89f505\">commit 3dda3b9</a></h2>\n<hr>\n<p>The Docker build for the worker image failed at <code>npm ci --ignore-scripts</code> with a\n403 from cdn.sheetjs.com. SheetJS distributes off the npm registry via their own\nCDN, and that CDN blocks datacentre IP ranges, so the tarball URL dependency can\nbe fetched from a developer machine but not from a GitHub Actions runner.</p>\n<p>Re-pinning to the registry was not a way out. <a href=\"mailto:xlsx@0.18.5\">xlsx@0.18.5</a> is the last version\nSheetJS published to npm and carries two unpatched high-severity advisories that\nnpm audit reports as fixAvailable: false, because there is nothing newer on the\nregistry to upgrade to:</p>\n<ul>\n<li>prototype pollution  GHSA-4r6h-8v6p-xvw6  (&lt; 0.19.3)</li>\n<li>ReDoS                GHSA-5pgg-2g8v-p4x9  (&lt; 0.20.2)</li>\n</ul>\n<p>The prototype pollution advisory is reachable: member-bulk-load parses a workbook\nthe user has just uploaded. So the library is replaced rather than re-pinned.</p>\n<p>exceljs is npm-hosted, maintained and free of known advisories. It was the single\nconsumer in the codebase, at one call site.</p>\n<p>To show the swap is behaviour-neutral, regression tests were written against the\nexisting xlsx reader and confirmed green first, then re-run unchanged against\nexceljs. That caught four cell types which sheet_to_json silently flattens but\nexceljs returns as objects, and which a direct swap would have written into member\nrecords as [object Object]:</p>\n<ul>\n<li>auto-hyperlinked email  -&gt;  {text, hyperlink}</li>\n<li>formula                 -&gt;  {formula, result}</li>\n<li>rich text               -&gt;  {richText: [...]}</li>\n<li>date                    -&gt;  Date, where xlsx gave an Excel serial</li>\n</ul>\n<p>Excel auto-hyperlinks email addresses, so the first of those was a live risk on a\nreal Ramblers export. The new reader normalises all four, and a test asserts no raw\ncell object can reach a row value.</p>\n<p>One divergence cannot be removed: exceljs collapses an empty-string cell to null, so\nthe empty-string versus absent-key distinction is lost. Rather than assume that was\nharmless, the row-to-member mapping was extracted and tested directly: every field\npasses through trim(), and trim(undefined) === trim(&quot;&quot;) === &quot;&quot;, so the member records\nare identical either way.</p>\n<p>Excel date cells are still read as serial numbers, as they were before. Real Ramblers\nexports store dates as strings, so this has never been hit. Preserved bug-for-bug so a\nsecurity fix does not also change data handling.</p>\n"}