compressing 1.10.3 → 1.10.5
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 +6 -2
- package/lib/utils.js +166 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# compressing
|
|
1
|
+
# compressing@1
|
|
2
2
|
|
|
3
3
|
[![NPM version][npm-image]][npm-url]
|
|
4
4
|
[![Test coverage][codecov-image]][codecov-url]
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
[download-image]: https://img.shields.io/npm/dm/compressing.svg?style=flat-square
|
|
15
15
|
[download-url]: https://npmjs.org/package/compressing
|
|
16
16
|
|
|
17
|
+
## ⚠️ Warning
|
|
18
|
+
|
|
19
|
+
**Version 1.x is no longer maintained. Please upgrade to version 2.x as soon as possible.**
|
|
20
|
+
|
|
17
21
|
The missing compressing and uncompressing lib for node.
|
|
18
22
|
|
|
19
23
|
Currently supported:
|
|
@@ -26,7 +30,7 @@ Currently supported:
|
|
|
26
30
|
## Install
|
|
27
31
|
|
|
28
32
|
```bash
|
|
29
|
-
npm install compressing
|
|
33
|
+
npm install compressing@1
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## Usage
|
package/lib/utils.js
CHANGED
|
@@ -5,6 +5,79 @@ const path = require('path');
|
|
|
5
5
|
const mkdirp = require('mkdirp');
|
|
6
6
|
const pump = require('pump');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Check if childPath is within parentPath (prevents path traversal attacks)
|
|
10
|
+
* @param {string} childPath - The path to check
|
|
11
|
+
* @param {string} parentPath - The parent directory path
|
|
12
|
+
* @returns {boolean} - True if childPath is within parentPath
|
|
13
|
+
*/
|
|
14
|
+
function isPathWithinParent(childPath, parentPath) {
|
|
15
|
+
const normalizedChild = path.resolve(childPath);
|
|
16
|
+
const normalizedParent = path.resolve(parentPath);
|
|
17
|
+
const parentWithSep = normalizedParent.endsWith(path.sep)
|
|
18
|
+
? normalizedParent
|
|
19
|
+
: normalizedParent + path.sep;
|
|
20
|
+
return normalizedChild === normalizedParent ||
|
|
21
|
+
normalizedChild.startsWith(parentWithSep);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check if the real filesystem path stays within parentDir,
|
|
26
|
+
* accounting for pre-existing symlinks on disk.
|
|
27
|
+
* Walks each path segment from parentDir to targetPath using lstat.
|
|
28
|
+
* If any segment is a symlink, resolves it and verifies it stays within parentDir.
|
|
29
|
+
* @param {string} targetPath - Absolute path to validate
|
|
30
|
+
* @param {string} parentDir - Absolute path of the extraction root
|
|
31
|
+
* @param {string} realParentDir - Pre-resolved real path of parentDir (handles OS-level symlinks like /var -> /private/var on macOS)
|
|
32
|
+
* @param {function} callback - callback(err, safe)
|
|
33
|
+
*/
|
|
34
|
+
function isRealPathSafe(targetPath, parentDir, realParentDir, callback) {
|
|
35
|
+
function isWithinParent(p) {
|
|
36
|
+
return isPathWithinParent(p, parentDir) || isPathWithinParent(p, realParentDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var relative = path.relative(parentDir, targetPath);
|
|
40
|
+
var segments = relative.split(path.sep);
|
|
41
|
+
var i = 0;
|
|
42
|
+
var current = parentDir;
|
|
43
|
+
|
|
44
|
+
function checkNext() {
|
|
45
|
+
if (i >= segments.length) return callback(null, true);
|
|
46
|
+
var segment = segments[i++];
|
|
47
|
+
if (!segment || segment === '.') return checkNext();
|
|
48
|
+
|
|
49
|
+
current = path.join(current, segment);
|
|
50
|
+
fs.lstat(current, function(err, stat) {
|
|
51
|
+
if (err) {
|
|
52
|
+
if (err.code === 'ENOENT') return callback(null, true); // doesn't exist yet, safe
|
|
53
|
+
// Fail closed: unexpected filesystem errors are unsafe
|
|
54
|
+
return callback(null, false);
|
|
55
|
+
}
|
|
56
|
+
if (!stat.isSymbolicLink()) return checkNext();
|
|
57
|
+
|
|
58
|
+
fs.realpath(current, function(err, resolved) {
|
|
59
|
+
if (err) {
|
|
60
|
+
if (err.code === 'ENOENT') {
|
|
61
|
+
// Dangling symlink - check textual target
|
|
62
|
+
return fs.readlink(current, function(err, linkTarget) {
|
|
63
|
+
if (err) return callback(null, false);
|
|
64
|
+
var absTarget = path.resolve(path.dirname(current), linkTarget);
|
|
65
|
+
callback(null, isWithinParent(absTarget));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Fail closed: unexpected errors during symlink resolution are unsafe
|
|
69
|
+
return callback(null, false);
|
|
70
|
+
}
|
|
71
|
+
if (!isWithinParent(resolved)) return callback(null, false);
|
|
72
|
+
current = resolved;
|
|
73
|
+
checkNext();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
checkNext();
|
|
79
|
+
}
|
|
80
|
+
|
|
8
81
|
// file/fileBuffer/stream
|
|
9
82
|
exports.sourceType = source => {
|
|
10
83
|
if (!source) return undefined;
|
|
@@ -93,58 +166,102 @@ exports.makeUncompressFn = StreamClass => {
|
|
|
93
166
|
mkdirp(destDir, err => {
|
|
94
167
|
if (err) return reject(err);
|
|
95
168
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
169
|
+
// Resolve destDir to absolute path for security validation
|
|
170
|
+
const resolvedDestDir = path.resolve(destDir);
|
|
171
|
+
|
|
172
|
+
// Resolve once for the entire extraction to handle OS-level symlinks
|
|
173
|
+
// (e.g. /var -> /private/var on macOS)
|
|
174
|
+
let realDestDir = resolvedDestDir;
|
|
175
|
+
fs.realpath(resolvedDestDir, (err, resolved) => {
|
|
176
|
+
if (!err) realDestDir = resolved;
|
|
177
|
+
|
|
178
|
+
let entryCount = 0;
|
|
179
|
+
let successCount = 0;
|
|
180
|
+
let isFinish = false;
|
|
181
|
+
function done() {
|
|
182
|
+
// resolve when both stream finish and file write finish
|
|
183
|
+
if (isFinish && entryCount === successCount) resolve();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
new StreamClass(opts)
|
|
187
|
+
.on('finish', () => {
|
|
188
|
+
isFinish = true;
|
|
189
|
+
done();
|
|
190
|
+
})
|
|
191
|
+
.on('error', reject)
|
|
192
|
+
.on('entry', (header, stream, next) => {
|
|
193
|
+
stream.on('end', next);
|
|
194
|
+
const destFilePath = path.join(resolvedDestDir, header.name);
|
|
195
|
+
const resolvedDestPath = path.resolve(destFilePath);
|
|
196
|
+
|
|
197
|
+
// Security: Validate that the entry path doesn't escape the destination directory
|
|
198
|
+
if (!isPathWithinParent(resolvedDestPath, resolvedDestDir)) {
|
|
199
|
+
console.warn('[compressing] Skipping entry with path traversal: "' + header.name + '" -> "' + resolvedDestPath + '"');
|
|
200
|
+
stream.resume();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
103
203
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})
|
|
109
|
-
.on('error', reject)
|
|
110
|
-
.on('entry', (header, stream, next) => {
|
|
111
|
-
stream.on('end', next);
|
|
112
|
-
const destFilePath = path.join(destDir, header.name);
|
|
113
|
-
|
|
114
|
-
if (header.type === 'file') {
|
|
115
|
-
const dir = path.dirname(destFilePath);
|
|
116
|
-
mkdirp(dir, err => {
|
|
117
|
-
if (err) return reject(err);
|
|
118
|
-
|
|
119
|
-
entryCount++;
|
|
120
|
-
pump(stream, fs.createWriteStream(destFilePath, { mode: opts.mode || header.mode }), err => {
|
|
121
|
-
if (err) return reject(err);
|
|
122
|
-
successCount++;
|
|
123
|
-
done();
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
} else if (header.type === 'symlink') {
|
|
127
|
-
const dir = path.dirname(destFilePath);
|
|
128
|
-
const target = path.resolve(dir, header.linkname);
|
|
129
|
-
entryCount++;
|
|
130
|
-
|
|
131
|
-
mkdirp(dir, err => {
|
|
132
|
-
if (err) return reject(err);
|
|
133
|
-
|
|
134
|
-
const relativeTarget = path.relative(dir, target);
|
|
135
|
-
fs.symlink(relativeTarget, destFilePath, err => {
|
|
136
|
-
if (err) return reject(err);
|
|
137
|
-
successCount++;
|
|
204
|
+
// Security: Validate no pre-existing symlink in the path escapes the extraction directory
|
|
205
|
+
isRealPathSafe(resolvedDestPath, resolvedDestDir, realDestDir, (err, safe) => {
|
|
206
|
+
if (err || !safe) {
|
|
207
|
+
console.warn('[compressing] Skipping entry "' + header.name + '": a symlink in its path resolves outside the extraction directory');
|
|
138
208
|
stream.resume();
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (header.type === 'file') {
|
|
213
|
+
const dir = path.dirname(destFilePath);
|
|
214
|
+
mkdirp(dir, err => {
|
|
215
|
+
if (err) return reject(err);
|
|
216
|
+
|
|
217
|
+
entryCount++;
|
|
218
|
+
pump(stream, fs.createWriteStream(destFilePath, { mode: opts.mode || header.mode }), err => {
|
|
219
|
+
if (err) return reject(err);
|
|
220
|
+
successCount++;
|
|
221
|
+
done();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
} else if (header.type === 'symlink') {
|
|
225
|
+
const dir = path.dirname(destFilePath);
|
|
226
|
+
const target = path.resolve(dir, header.linkname);
|
|
227
|
+
|
|
228
|
+
// Security: Validate that the symlink target doesn't escape the destination directory
|
|
229
|
+
if (!isPathWithinParent(target, resolvedDestDir)) {
|
|
230
|
+
console.warn('[compressing] Skipping symlink "' + header.name + '": target "' + target + '" escapes extraction directory');
|
|
231
|
+
stream.resume();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Security: Validate no pre-existing symlink in the target path escapes the extraction directory
|
|
236
|
+
isRealPathSafe(target, resolvedDestDir, realDestDir, (err, targetSafe) => {
|
|
237
|
+
if (err || !targetSafe) {
|
|
238
|
+
console.warn('[compressing] Skipping symlink "' + header.name + '": target resolves outside extraction directory via existing symlink');
|
|
239
|
+
stream.resume();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
entryCount++;
|
|
244
|
+
|
|
245
|
+
mkdirp(dir, err => {
|
|
246
|
+
if (err) return reject(err);
|
|
247
|
+
|
|
248
|
+
const relativeTarget = path.relative(dir, target);
|
|
249
|
+
fs.symlink(relativeTarget, destFilePath, err => {
|
|
250
|
+
if (err) return reject(err);
|
|
251
|
+
successCount++;
|
|
252
|
+
stream.resume();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
} else { // directory
|
|
257
|
+
mkdirp(destFilePath, err => {
|
|
258
|
+
if (err) return reject(err);
|
|
259
|
+
stream.resume();
|
|
260
|
+
});
|
|
261
|
+
}
|
|
145
262
|
});
|
|
146
|
-
}
|
|
147
|
-
|
|
263
|
+
});
|
|
264
|
+
});
|
|
148
265
|
});
|
|
149
266
|
});
|
|
150
267
|
};
|