diff --git a/patches/decompress@4.2.1.patch b/patches/decompress@4.2.1.patch new file mode 100644 index 00000000..20e7c442 --- /dev/null +++ b/patches/decompress@4.2.1.patch @@ -0,0 +1,78 @@ +diff --git a/index.js b/index.js +index 6aa67ca9cda4bac1262915ca6e47e97b224d336e..17fcc21d96543efa7d1bdd4266e4c1a2b40ef635 100644 +--- a/index.js ++++ b/index.js +@@ -26,7 +26,8 @@ const safeMakeDir = (dir, realOutputPath) => { + return safeMakeDir(parent, realOutputPath); + }) + .then(realParentPath => { +- if (realParentPath.indexOf(realOutputPath) !== 0) { ++ const rel = path.relative(realOutputPath, realParentPath); ++ if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw (new Error('Refusing to create a directory outside the output path.')); + } + +@@ -51,6 +52,35 @@ const preventWritingThroughSymlink = (destination, realOutputPath) => { + }); + }; + ++// Security: strip setuid, setgid, and sticky bits from extracted file modes. ++// The original code used `x.mode & ~process.umask()` which only clears bits ++// already masked by the process umask, leaving setuid/setgid/sticky intact. ++// A crafted archive could create a setuid binary on extraction, which is ++// especially dangerous when extraction runs as root (CI, containers). ++const safeMode = (mode) => (mode & ~process.umask()) & 0o777 & ~0o7000; ++ ++// Security: resolve a symlink/hardlink target to its realpath and verify ++// it stays inside realOutputPath. Prevents a crafted archive from creating ++// a link to /etc/passwd (hardlink) or a symlink to /tmp (symlink) inside ++// the output directory. ++const resolveAndCheckLinkTarget = (linkname, realOutputPath) => { ++ const resolved = path.resolve(realOutputPath, linkname); ++ return fsP.realpath(resolved) ++ .catch(_ => { ++ // Target doesn't exist yet — for hardlinks, check the parent dir; ++ // for symlinks, the target is checked at write time below. ++ return null; ++ }) ++ .then(realTarget => { ++ if (realTarget) { ++ const rel = path.relative(realOutputPath, realTarget); ++ if (rel.startsWith('..') || path.isAbsolute(rel)) { ++ throw new Error('Refusing to create a link to outside output directory: ' + realTarget); ++ } ++ } ++ }); ++}; ++ + const extractFile = (input, output, opts) => runPlugins(input, opts).then(files => { + if (opts.strip > 0) { + files = files +@@ -75,7 +105,7 @@ const extractFile = (input, output, opts) => runPlugins(input, opts).then(files + + return Promise.all(files.map(x => { + const dest = path.join(output, x.path); +- const mode = x.mode & ~process.umask(); ++ const mode = safeMode(x.mode); + const now = new Date(); + + if (x.type === 'directory') { +@@ -103,11 +133,17 @@ const extractFile = (input, output, opts) => runPlugins(input, opts).then(files + .then(realOutputPath => { + return fsP.realpath(path.dirname(dest)) + .then(realDestinationDir => { +- if (realDestinationDir.indexOf(realOutputPath) !== 0) { ++ const rel = path.relative(realOutputPath, realDestinationDir); ++ if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw (new Error('Refusing to write outside output directory: ' + realDestinationDir)); + } + }); + }) ++ .then(realOutputPath => { ++ if (x.type === 'link' || x.type === 'symlink') { ++ return resolveAndCheckLinkTarget(x.linkname, realOutputPath); ++ } ++ }) + .then(() => { + if (x.type === 'link') { + return fsP.link(x.linkname, dest); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1656ce84..78b9fa76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ overrides: tough-cookie@<4.1.3: '>=4.1.3' trim-newlines@<3.0.1: '>=3.0.1' +patchedDependencies: + decompress@4.2.1: 45158ed496346a01b560e676c853793c22191f0f6056b5633694f408fcb7372b + importers: .: @@ -15858,7 +15861,7 @@ snapshots: bin-build@3.0.0: dependencies: - decompress: 4.2.1 + decompress: 4.2.1(patch_hash=45158ed496346a01b560e676c853793c22191f0f6056b5633694f408fcb7372b) download: 6.2.5 execa: 0.7.0 p-map-series: 1.0.0 @@ -16643,7 +16646,7 @@ snapshots: pify: 2.3.0 yauzl: 2.10.0 - decompress@4.2.1: + decompress@4.2.1(patch_hash=45158ed496346a01b560e676c853793c22191f0f6056b5633694f408fcb7372b): dependencies: decompress-tar: 4.1.1 decompress-tarbz2: 4.1.1 @@ -16795,7 +16798,7 @@ snapshots: dependencies: caw: 2.0.1 content-disposition: 0.5.4 - decompress: 4.2.1 + decompress: 4.2.1(patch_hash=45158ed496346a01b560e676c853793c22191f0f6056b5633694f408fcb7372b) ext-name: 5.0.0 file-type: 5.2.0 filenamify: 2.1.0 @@ -16810,7 +16813,7 @@ snapshots: archive-type: 4.0.0 caw: 2.0.1 content-disposition: 0.5.4 - decompress: 4.2.1 + decompress: 4.2.1(patch_hash=45158ed496346a01b560e676c853793c22191f0f6056b5633694f408fcb7372b) ext-name: 5.0.0 file-type: 8.1.0 filenamify: 2.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 369ef593..4df276ee 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -87,4 +87,7 @@ overrides: tough-cookie@<4.1.3: ">=4.1.3" trim-newlines@<3.0.1: ">=3.0.1" +patchedDependencies: + decompress@4.2.1: patches/decompress@4.2.1.patch + shamefullyHoist: true