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.
Files changed (3) hide show
  1. package/README.md +6 -2
  2. package/lib/utils.js +166 -49
  3. 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
- let entryCount = 0;
97
- let successCount = 0;
98
- let isFinish = false;
99
- function done() {
100
- // resolve when both stream finish and file write finish
101
- if (isFinish && entryCount === successCount) resolve();
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
- new StreamClass(opts)
105
- .on('finish', () => {
106
- isFinish = true;
107
- done();
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
- } else { // directory
142
- mkdirp(destFilePath, err => {
143
- if (err) return reject(err);
144
- stream.resume();
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compressing",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "Everything you need for compressing and uncompressing",
5
5
  "main": "index.js",
6
6
  "scripts": {