Skip to content

Commit fe8cd57

Browse files
committed
prevent extraction in excessively deep subfolders
This sets the limit at 1024 subfolders nesting by default, but that can be dropped down, or set to Infinity to remove the limitation.
1 parent fe7ebfd commit fe8cd57

File tree

5 files changed

+94
-6
lines changed

5 files changed

+94
-6
lines changed

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ Handlers receive 3 arguments:
115115
encountered an error which prevented it from being unpacked. This occurs
116116
when:
117117
- an unrecoverable fs error happens during unpacking,
118+
- an entry is trying to extract into an excessively deep
119+
location (by default, limited to 1024 subfolders),
118120
- an entry has `..` in the path and `preservePaths` is not set, or
119121
- an entry is extracting through a symbolic link, when `preservePaths` is
120122
not set.
@@ -427,6 +429,10 @@ The following options are supported:
427429
`process.umask()` to determine the default umask value, since tar will
428430
extract with whatever mode is provided, and let the process `umask` apply
429431
normally.
432+
- `maxDepth` The maximum depth of subfolders to extract into. This
433+
defaults to 1024. Anything deeper than the limit will raise a
434+
warning and skip the entry. Set to `Infinity` to remove the
435+
limitation.
430436

431437
The following options are mostly internal, but can be modified in some
432438
advanced use cases, such as re-using caches between runs.
@@ -749,6 +755,10 @@ Most unpack errors will cause a `warn` event to be emitted. If the
749755
`process.umask()` to determine the default umask value, since tar will
750756
extract with whatever mode is provided, and let the process `umask` apply
751757
normally.
758+
- `maxDepth` The maximum depth of subfolders to extract into. This
759+
defaults to 1024. Anything deeper than the limit will raise a
760+
warning and skip the entry. Set to `Infinity` to remove the
761+
limitation.
752762

753763
### class tar.Unpack.Sync
754764

lib/unpack.js

+22-5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const crypto = require('crypto')
4848
const getFlag = require('./get-write-flag.js')
4949
const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform
5050
const isWindows = platform === 'win32'
51+
const DEFAULT_MAX_DEPTH = 1024
5152

5253
// Unlinks on Windows are not atomic.
5354
//
@@ -181,6 +182,12 @@ class Unpack extends Parser {
181182
this.processGid = (this.preserveOwner || this.setOwner) && process.getgid ?
182183
process.getgid() : null
183184

185+
// prevent excessively deep nesting of subfolders
186+
// set to `Infinity` to remove this restriction
187+
this.maxDepth = typeof opt.maxDepth === 'number'
188+
? opt.maxDepth
189+
: DEFAULT_MAX_DEPTH
190+
184191
// mostly just for testing, but useful in some cases.
185192
// Forcibly trigger a chown on every entry, no matter what
186193
this.forceChown = opt.forceChown === true
@@ -238,13 +245,13 @@ class Unpack extends Parser {
238245
}
239246

240247
[CHECKPATH] (entry) {
248+
const p = normPath(entry.path)
249+
const parts = p.split('/')
250+
241251
if (this.strip) {
242-
const parts = normPath(entry.path).split('/')
243252
if (parts.length < this.strip) {
244253
return false
245254
}
246-
entry.path = parts.slice(this.strip).join('/')
247-
248255
if (entry.type === 'Link') {
249256
const linkparts = normPath(entry.linkpath).split('/')
250257
if (linkparts.length >= this.strip) {
@@ -253,11 +260,21 @@ class Unpack extends Parser {
253260
return false
254261
}
255262
}
263+
parts.splice(0, this.strip)
264+
entry.path = parts.join('/')
265+
}
266+
267+
if (isFinite(this.maxDepth) && parts.length > this.maxDepth) {
268+
this.warn('TAR_ENTRY_ERROR', 'path excessively deep', {
269+
entry,
270+
path: p,
271+
depth: parts.length,
272+
maxDepth: this.maxDepth,
273+
})
274+
return false
256275
}
257276

258277
if (!this.preservePaths) {
259-
const p = normPath(entry.path)
260-
const parts = p.split('/')
261278
if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) {
262279
this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
263280
entry,

test/fixtures/excessively-deep.tar

440 KB
Binary file not shown.

test/parse.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ t.test('truncated gzip input', t => {
646646
p.write(tgz.slice(split))
647647
p.end()
648648
t.equal(aborted, true, 'aborted writing')
649-
t.same(warnings, ['zlib: incorrect data check'])
649+
t.match(warnings, [/^zlib: /])
650650
t.end()
651651
})
652652

test/unpack.js

+61
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const mkdirp = require('mkdirp')
2222
const mutateFS = require('mutate-fs')
2323
const eos = require('end-of-stream')
2424
const normPath = require('../lib/normalize-windows-path.js')
25+
const ReadEntry = require('../lib/read-entry.js')
2526

2627
// On Windows in particular, the "really deep folder path" file
2728
// often tends to cause problems, which don't indicate a failure
@@ -3235,3 +3236,63 @@ t.test('recognize C:.. as a dot path part', t => {
32353236

32363237
t.end()
32373238
})
3239+
3240+
t.test('excessively deep subfolder nesting', async t => {
3241+
const tf = path.resolve(fixtures, 'excessively-deep.tar')
3242+
const data = fs.readFileSync(tf)
3243+
const warnings = []
3244+
const onwarn = (c, w, { entry, path, depth, maxDepth }) =>
3245+
warnings.push([c, w, { entry, path, depth, maxDepth }])
3246+
3247+
const check = (t, maxDepth = 1024) => {
3248+
t.match(warnings, [
3249+
['TAR_ENTRY_ERROR',
3250+
'path excessively deep',
3251+
{
3252+
entry: ReadEntry,
3253+
path: /^\.(\/a){1024,}\/foo.txt$/,
3254+
depth: 222372,
3255+
maxDepth,
3256+
}
3257+
]
3258+
])
3259+
warnings.length = 0
3260+
t.end()
3261+
}
3262+
3263+
t.test('async', t => {
3264+
const cwd = t.testdir()
3265+
new Unpack({
3266+
cwd,
3267+
onwarn
3268+
}).on('end', () => check(t)).end(data)
3269+
})
3270+
3271+
t.test('sync', t => {
3272+
const cwd = t.testdir()
3273+
new UnpackSync({
3274+
cwd,
3275+
onwarn
3276+
}).end(data)
3277+
check(t)
3278+
})
3279+
3280+
t.test('async set md', t => {
3281+
const cwd = t.testdir()
3282+
new Unpack({
3283+
cwd,
3284+
onwarn,
3285+
maxDepth: 64,
3286+
}).on('end', () => check(t, 64)).end(data)
3287+
})
3288+
3289+
t.test('sync set md', t => {
3290+
const cwd = t.testdir()
3291+
new UnpackSync({
3292+
cwd,
3293+
onwarn,
3294+
maxDepth: 64,
3295+
}).end(data)
3296+
check(t, 64)
3297+
})
3298+
})

0 commit comments

Comments
 (0)