Skip to content

fix(backend-shared): replace unmaintained decompress with @xhmikosr/d… - #11529

Open
allanmaclean wants to merge 1 commit into
cube-js:masterfrom
allanmaclean:fix/decompress
Open

fix(backend-shared): replace unmaintained decompress with @xhmikosr/d…#11529
allanmaclean wants to merge 1 commit into
cube-js:masterfrom
allanmaclean:fix/decompress

Conversation

@allanmaclean

Copy link
Copy Markdown

Check List

  • Tests have been run in packages where changes have been made if available
  • Linter has been run for changed code
  • Tests for the changes have been added if not covered yet
  • Docs have been added / updated if required — n/a, no user-facing behaviour change

Issue Reference this PR resolves

#11264

Description of Changes Made

@cubejs-backend/shared and @cubejs-backend/templates both depended on decompress@^4.2.1,
which is unmaintained and carries an unpatched path-traversal advisory
(GHSA-mp2f-45pm-3cg9
/ CVE-2026-53486) — archive extraction can create files and links outside the target directory.
This swaps both onto the maintained fork @xhmikosr/decompress@^11.1.4, where it is patched.

The fork is ESM-only ("type": "module", Node >=20), which is why downstream consumers can't
just override the dependency themselves. Rather than migrating any package to ESM, the ESM
boundary is confined to a single function.

Approach

@cubejs-backend/shared gains one exported helper, extractArchive(archivePath, cwd):

export async function extractArchive(archivePath: string, cwd: string): Promise<void> {
  const { default: decompress } = await import('@xhmikosr/decompress');

  await decompress(archivePath, cwd);
}

The subtlety: under the repo's default "module": "commonjs", TypeScript downlevels that
import() to Promise.resolve().then(() => require(...)), which defeats the purpose. So
packages/cubejs-backend-shared/tsconfig.json switches to module/moduleResolution: "Node16".
The package still emits CommonJS — only the import() is left intact:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const node_fetch_1 = __importStar(require("node-fetch"));   // ordinary imports -> require()
// ...
const { default: decompress } = await import('@xhmikosr/decompress');  // ESM dep stays import()

No package migrates to ESM, and no new Function('return import(...)') shim is needed.

@cubejs-backend/templates (which already depends on @cubejs-backend/shared) just calls the
helper, so it drops both decompress and decompress-targz with no tsconfig change of its own.
@xhmikosr/decompress bundles @xhmikosr/decompress-targz in its default plugin set, so the
explicit decompress-targz plugin is no longer required.

Both packages already declare "node": ">=20.0.0", matching the fork's engine requirement.

Changes

File Change
cubejs-backend-shared/package.json decompress + @types/decompress@xhmikosr/decompress
cubejs-backend-shared/tsconfig.json module/moduleResolution: Node16
cubejs-backend-shared/src/http-utils.ts new extractArchive helper; downloadAndExtractFile uses it
cubejs-backend-shared/src/xhmikosr-decompress.d.ts ambient types (the fork ships none)
cubejs-backend-shared/tsconfig.jest.json package-local ts-jest config (new)
cubejs-backend-shared/jest.config.js point ts-jest at the above
cubejs-backend-shared/test/http-utils.test.ts tests (new)
cubejs-templates/package.json drop decompress + decompress-targz
cubejs-templates/src/PackageFetcher.ts use extractArchive
yarn.lock regenerated

Why the package-local tsconfig.jest.json

@xhmikosr/decompress ships no type declarations, so the package needs an ambient declare module. ts-jest was pointed at the root tsconfig.jest.json, whose include doesn't cover
this package's src, so the declaration wasn't part of the ts-jest program and 9 existing suites
failed to compile. Extending the package tsconfig keeps include intact. It pins module back to
CommonJS because Jest runs sources through its own CommonJS runtime — the Node16 emit only matters
for the published dist build.

Testing

Two tests in packages/cubejs-backend-shared/test/http-utils.test.ts:

  1. Runtime test — extracts a real .tar.gz fixture through the compiled CommonJS output,
    asserting nested paths and file contents survive.
  2. Emit guard — asserts the built dist contains a native await import('@xhmikosr/decompress')
    and not a downleveled require().

The runtime test runs the extraction in a child node process rather than in-process, because
Jest's CommonJS runtime intercepts dynamic import() and routes it through its own loader, which
cannot load an ESM-only package (this holds even with --experimental-vm-modules, since the
importing module is CommonJS). Running it as a plain node child process is what makes the
CommonJS → ESM hop observable. Migrating this package's Jest setup to native ESM would be a much
larger change to a 394-test suite, and isn't needed for the fix.

The emit guard is not redundant: Node.js >=22.12 can require() ESM, so if someone drops the
Node16 setting the runtime test would still pass on CI's Node 24 while the package broke on the
supported Node 20 floor. Verified by reverting the tsconfig — the runtime test passed and only the
guard failed.

Verified locally:

  • yarn tsc across the whole monorepo — clean (many packages consume shared, so the Node16
    switch was the main risk; no downstream breakage).
  • yarn unit in cubejs-backend-shared — 16 suites, 394 tests passing.
  • yarn lint on both changed packages, plus npmPkgJsonLint — clean.
  • The extraction path exercised directly on Node 20.15.1 and Node 24.19.0. Node 20 is the
    meaningful one: it has no require(esm), so it proves the import() genuinely survived.

Notes for reviewers

  • A transitive decompress@4.2.1 remains in yarn.lock and isn't removable from this PR. It
    comes from @cubejs-backend/native@cubejs-infra/post-installer@cubejs-backend/shared@0.33.20,
    i.e. an old published build of this very package pinned by post-installer. Clearing it needs
    a @cubejs-infra/post-installer release bumping that pin. Cube's own source tree no longer
    references decompress.
  • Minor behaviour change in templates: extraction previously restricted itself to the
    decompress-targz plugin; it now uses the fork's default plugin set (tar, tar.bz2, tar.gz, zip).
    The input is always a master.tar.gz from GitHub, so this is not expected to matter. shared
    had no plugins option before, so its accepted formats are unchanged.
  • extractArchive is newly exported from @cubejs-backend/shared. This is additive public API,
    chosen so the CommonJS → ESM boundary exists in exactly one place rather than being duplicated in
    every package that needs to extract an archive.

AI disclosure: Opus 5 used in the creation of this PR.

@allanmaclean
allanmaclean requested review from a team as code owners August 11, 2026 17:12
@github-actions github-actions Bot added javascript Pull requests that update Javascript code pr:community Contribution from Cube.js community members. labels Aug 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update Javascript code pr:community Contribution from Cube.js community members.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant