Skip to content

Commit 340eb28

Browse files
committed
fix: sanitize absolute linkpaths properly
Fix: GHSA-8qq5-rm4j-mr97
1 parent 8bb83f7 commit 340eb28

File tree

2 files changed

+94
-26
lines changed

2 files changed

+94
-26
lines changed

src/unpack.ts

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const SYMLINK = Symbol('symlink')
3333
const HARDLINK = Symbol('hardlink')
3434
const UNSUPPORTED = Symbol('unsupported')
3535
const CHECKPATH = Symbol('checkPath')
36+
const STRIPABSOLUTEPATH = Symbol('stripAbsolutePath')
3637
const MKDIR = Symbol('mkdir')
3738
const ONERROR = Symbol('onError')
3839
const PENDING = Symbol('pending')
@@ -263,6 +264,46 @@ export class Unpack extends Parser {
263264
}
264265
}
265266

267+
// return false if we need to skip this file
268+
// return true if the field was successfully sanitized
269+
[STRIPABSOLUTEPATH](
270+
entry: ReadEntry,
271+
field: 'path' | 'linkpath',
272+
): boolean {
273+
const path = entry[field]
274+
if (!path || this.preservePaths) return true
275+
276+
const parts = path.split('/')
277+
if (
278+
parts.includes('..') ||
279+
/* c8 ignore next */
280+
(isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
281+
) {
282+
this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
283+
entry,
284+
[field]: path,
285+
})
286+
// not ok!
287+
return false
288+
}
289+
290+
// strip off the root
291+
const [root, stripped] = stripAbsolutePath(path)
292+
if (root) {
293+
// ok, but triggers warning about stripping root
294+
entry[field] = String(stripped)
295+
this.warn(
296+
'TAR_ENTRY_INFO',
297+
`stripping ${root} from absolute ${field}`,
298+
{
299+
entry,
300+
[field]: path,
301+
},
302+
)
303+
}
304+
return true
305+
}
306+
266307
[CHECKPATH](entry: ReadEntry) {
267308
const p = normalizeWindowsPath(entry.path)
268309
const parts = p.split('/')
@@ -295,32 +336,11 @@ export class Unpack extends Parser {
295336
return false
296337
}
297338

298-
if (!this.preservePaths) {
299-
if (
300-
parts.includes('..') ||
301-
/* c8 ignore next */
302-
(isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
303-
) {
304-
this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
305-
entry,
306-
path: p,
307-
})
308-
return false
309-
}
310-
311-
// strip off the root
312-
const [root, stripped] = stripAbsolutePath(p)
313-
if (root) {
314-
entry.path = String(stripped)
315-
this.warn(
316-
'TAR_ENTRY_INFO',
317-
`stripping ${root} from absolute path`,
318-
{
319-
entry,
320-
path: p,
321-
},
322-
)
323-
}
339+
if (
340+
!this[STRIPABSOLUTEPATH](entry, 'path') ||
341+
!this[STRIPABSOLUTEPATH](entry, 'linkpath')
342+
) {
343+
return false
324344
}
325345

326346
if (path.isAbsolute(entry.path)) {

test/ghsa-8qq5-rm4j-mr97.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { readFileSync, readlinkSync, writeFileSync } from 'fs'
2+
import { resolve } from 'path'
3+
import t from 'tap'
4+
import { Header, x } from 'tar'
5+
6+
const targetSym = '/some/absolute/path'
7+
8+
const getExploitTar = () => {
9+
const exploitTar = Buffer.alloc(512 + 512 + 1024)
10+
11+
new Header({
12+
path: 'exploit_hard',
13+
type: 'Link',
14+
size: 0,
15+
linkpath: resolve(t.testdirName, 'secret.txt'),
16+
}).encode(exploitTar, 0)
17+
18+
new Header({
19+
path: 'exploit_sym',
20+
type: 'SymbolicLink',
21+
size: 0,
22+
linkpath: targetSym,
23+
}).encode(exploitTar, 512)
24+
25+
return exploitTar
26+
}
27+
28+
const dir = t.testdir({
29+
'secret.txt': 'ORIGINAL DATA',
30+
'exploit.tar': getExploitTar(),
31+
out_repro: {},
32+
})
33+
34+
const out = resolve(dir, 'out_repro')
35+
const tarFile = resolve(dir, 'exploit.tar')
36+
37+
t.test('verify that linkpaths get sanitized properly', async t => {
38+
await x({
39+
cwd: out,
40+
file: tarFile,
41+
preservePaths: false,
42+
})
43+
44+
writeFileSync(resolve(out, 'exploit_hard'), 'OVERWRITTEN')
45+
t.equal(readFileSync(resolve(dir, 'secret.txt'), 'utf8'), 'ORIGINAL DATA')
46+
47+
t.not(readlinkSync(resolve(out, 'exploit_sym')), targetSym)
48+
})

0 commit comments

Comments
 (0)