compressing 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/lib/utils.js +40 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,7 +169,7 @@ const urllib = require('urllib');
|
|
|
169
169
|
const targetDir = require('os').tmpdir();
|
|
170
170
|
const compressing = require('compressing');
|
|
171
171
|
|
|
172
|
-
urllib.request('http://registry.npmjs.org/
|
|
172
|
+
urllib.request('http://registry.npmjs.org/compressing/-/compressing-2.0.0.tgz', {
|
|
173
173
|
streaming: true,
|
|
174
174
|
followRedirect: true,
|
|
175
175
|
})
|
|
@@ -280,6 +280,7 @@ Params
|
|
|
280
280
|
- Korean: cp949, euc-kr
|
|
281
281
|
- Japanese: sjis (shift_jis), cp932, euc-jp
|
|
282
282
|
- Chinese: gbk, gb18030, gb2312, cp936, hkscs, big5, cp950
|
|
283
|
+
- opts.strip {Number} - Strip leading path segments when extracting (tar/tgz/zip). Default is 0.
|
|
283
284
|
|
|
284
285
|
### FileStream
|
|
285
286
|
|
|
@@ -359,6 +360,7 @@ __Constructor__
|
|
|
359
360
|
Common params:
|
|
360
361
|
|
|
361
362
|
- opts.source {String|Buffer|Stream} - source to be uncompressed, could be a file path, buffer, or a readable stream.
|
|
363
|
+
- opts.strip {Number} - Strip leading path segments when extracting (tar/tgz/zip). Default is 0.
|
|
362
364
|
|
|
363
365
|
__CAUTION for zip.UncompressStream__
|
|
364
366
|
|
package/lib/utils.js
CHANGED
|
@@ -4,6 +4,22 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { pipeline: pump } = require('stream');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Check if childPath is within parentPath (prevents path traversal attacks)
|
|
9
|
+
* @param {string} childPath - The path to check
|
|
10
|
+
* @param {string} parentPath - The parent directory path
|
|
11
|
+
* @returns {boolean} - True if childPath is within parentPath
|
|
12
|
+
*/
|
|
13
|
+
function isPathWithinParent(childPath, parentPath) {
|
|
14
|
+
const normalizedChild = path.resolve(childPath);
|
|
15
|
+
const normalizedParent = path.resolve(parentPath);
|
|
16
|
+
const parentWithSep = normalizedParent.endsWith(path.sep)
|
|
17
|
+
? normalizedParent
|
|
18
|
+
: normalizedParent + path.sep;
|
|
19
|
+
return normalizedChild === normalizedParent ||
|
|
20
|
+
normalizedChild.startsWith(parentWithSep);
|
|
21
|
+
}
|
|
22
|
+
|
|
7
23
|
// file/fileBuffer/stream
|
|
8
24
|
exports.sourceType = source => {
|
|
9
25
|
if (!source) return undefined;
|
|
@@ -88,10 +104,17 @@ exports.makeUncompressFn = StreamClass => {
|
|
|
88
104
|
throw error;
|
|
89
105
|
}
|
|
90
106
|
|
|
107
|
+
const strip = opts.strip ? Number(opts.strip) : 0;
|
|
108
|
+
// Strip is handled here in makeUncompressFn, so remove it from opts to avoid passing to UncompressStream
|
|
109
|
+
delete opts.strip;
|
|
110
|
+
|
|
91
111
|
return new Promise((resolve, reject) => {
|
|
92
112
|
fs.mkdir(destDir, { recursive: true }, err => {
|
|
93
113
|
if (err) return reject(err);
|
|
94
114
|
|
|
115
|
+
// Resolve destDir to absolute path for security validation
|
|
116
|
+
const resolvedDestDir = path.resolve(destDir);
|
|
117
|
+
|
|
95
118
|
let entryCount = 0;
|
|
96
119
|
let successCount = 0;
|
|
97
120
|
let isFinish = false;
|
|
@@ -108,7 +131,15 @@ exports.makeUncompressFn = StreamClass => {
|
|
|
108
131
|
.on('error', reject)
|
|
109
132
|
.on('entry', (header, stream, next) => {
|
|
110
133
|
stream.on('end', next);
|
|
111
|
-
const destFilePath = path.join(
|
|
134
|
+
const destFilePath = path.join(resolvedDestDir, stripFileName(strip, header.name, header.type));
|
|
135
|
+
const resolvedDestPath = path.resolve(destFilePath);
|
|
136
|
+
|
|
137
|
+
// Security: Validate that the entry path doesn't escape the destination directory
|
|
138
|
+
if (!isPathWithinParent(resolvedDestPath, resolvedDestDir)) {
|
|
139
|
+
console.warn(`[compressing] Skipping entry with path traversal: "${header.name}" -> "${resolvedDestPath}"`);
|
|
140
|
+
stream.resume();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
112
143
|
|
|
113
144
|
if (header.type === 'file') {
|
|
114
145
|
const dir = path.dirname(destFilePath);
|
|
@@ -125,6 +156,14 @@ exports.makeUncompressFn = StreamClass => {
|
|
|
125
156
|
} else if (header.type === 'symlink') {
|
|
126
157
|
const dir = path.dirname(destFilePath);
|
|
127
158
|
const target = path.resolve(dir, header.linkname);
|
|
159
|
+
|
|
160
|
+
// Security: Validate that the symlink target doesn't escape the destination directory
|
|
161
|
+
if (!isPathWithinParent(target, resolvedDestDir)) {
|
|
162
|
+
console.warn(`[compressing] Skipping symlink "${header.name}": target "${target}" escapes extraction directory`);
|
|
163
|
+
stream.resume();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
entryCount++;
|
|
129
168
|
|
|
130
169
|
fs.mkdir(dir, { recursive: true }, err => {
|