Skip to content

Commit 631ca54

Browse files
node-tar: Fix 6 CVE vulnerabilities (6 HIGH)
Fix the following security vulnerabilities: - CVE-2026-23745: Insufficient Link Path Sanitization (HIGH) - CVE-2026-23950: Race Condition in node-tar Path Reservations (HIGH) - CVE-2026-29786: Hardlink Path Traversal via Drive-Relative Linkpath (HIGH) - CVE-2026-26960: Arbitrary File Read/Write via Hardlink Target Escape (HIGH) - CVE-2026-24842: Arbitrary File Read/Overwrite via Hardlink Path Traversal (HIGH) - CVE-2026-31802: Symlink Path Traversal via Drive-Relative Linkpath (HIGH) Patches imported from Debian Salsa: - https://salsa.debian.org/js-team/node-tar Also includes: - Adapt to chownr 3 (named exports) - api-backward-compatibility patch Upstream: https://github.com/isaacs/node-tar Generated-By: uos/glm-5.1 Co-Authored-By: hudeng <hudeng@deepin.org>
1 parent 57d6f60 commit 631ca54

10 files changed

Lines changed: 549 additions & 4 deletions

debian/changelog

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
node-tar (6.2.1+~cs7.0.8-1deepin1) unstable; urgency=medium
2+
3+
* Fix CVE-2026-23745: Insufficient Link Path Sanitization
4+
* Fix CVE-2026-23950: Race Condition in node-tar Path Reservations
5+
* Fix CVE-2026-29786: Hardlink Path Traversal via Drive-Relative
6+
Linkpath
7+
* Fix CVE-2026-26960: Arbitrary File Read/Write via Hardlink Target
8+
Escape
9+
* Fix CVE-2026-24842: Arbitrary File Read/Overwrite via Hardlink Path
10+
Traversal
11+
* Fix CVE-2026-31802: Symlink Path Traversal via Drive-Relative
12+
Linkpath
13+
* Adapt to chownr 3 (named exports)
14+
15+
-- deepin-ci-robot <packages@deepin.org> Fri, 17 Apr 2026 02:23:36 +0800
16+
117
node-tar (6.2.1+~cs7.0.8-1) unstable; urgency=medium
218

319
* New upstream version
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
Description: sanitize absolute linkpaths properly
2+
Author: isaacs <i@izs.me>
3+
Origin: upstream, https://github.com/isaacs/node-tar/commit/340eb285
4+
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-8qq5-rm4j-mr97
5+
Forwarded: not-needed
6+
Applied-Upstream: 7.5.3, commit:340eb285
7+
Reviewed-By: Xavier Guimard <yadd@debian.org>
8+
Last-Update: 2026-01-17
9+
10+
--- a/lib/unpack.js
11+
+++ b/lib/unpack.js
12+
@@ -32,6 +32,7 @@
13+
const HARDLINK = Symbol('hardlink')
14+
const UNSUPPORTED = Symbol('unsupported')
15+
const CHECKPATH = Symbol('checkPath')
16+
+const STRIPABSOLUTEPATH = Symbol('stripAbsolutePath')
17+
const MKDIR = Symbol('mkdir')
18+
const ONERROR = Symbol('onError')
19+
const PENDING = Symbol('pending')
20+
@@ -244,6 +245,43 @@
21+
}
22+
}
23+
24+
+ // return false if we need to skip this file
25+
+ // return true if the field was successfully sanitized
26+
+ [STRIPABSOLUTEPATH]( entry, field ) {
27+
+ const path = entry[field]
28+
+ if (!path || this.preservePaths) return true
29+
+
30+
+ const parts = path.split('/')
31+
+ if (
32+
+ parts.includes('..') ||
33+
+ /* c8 ignore next */
34+
+ (isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
35+
+ ) {
36+
+ this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
37+
+ entry,
38+
+ [field]: path,
39+
+ })
40+
+ // not ok!
41+
+ return false
42+
+ }
43+
+
44+
+ // strip off the root
45+
+ const [root, stripped] = stripAbsolutePath(path)
46+
+ if (root) {
47+
+ // ok, but triggers warning about stripping root
48+
+ entry[field] = String(stripped)
49+
+ this.warn(
50+
+ 'TAR_ENTRY_INFO',
51+
+ `stripping ${root} from absolute ${field}`,
52+
+ {
53+
+ entry,
54+
+ [field]: path,
55+
+ },
56+
+ )
57+
+ }
58+
+ return true
59+
+ }
60+
+
61+
[CHECKPATH] (entry) {
62+
const p = normPath(entry.path)
63+
const parts = p.split('/')
64+
@@ -274,24 +312,11 @@
65+
return false
66+
}
67+
68+
- if (!this.preservePaths) {
69+
- if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) {
70+
- this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
71+
- entry,
72+
- path: p,
73+
- })
74+
- return false
75+
- }
76+
-
77+
- // strip off the root
78+
- const [root, stripped] = stripAbsolutePath(p)
79+
- if (root) {
80+
- entry.path = stripped
81+
- this.warn('TAR_ENTRY_INFO', `stripping ${root} from absolute path`, {
82+
- entry,
83+
- path: p,
84+
- })
85+
- }
86+
+ if (
87+
+ !this[STRIPABSOLUTEPATH](entry, 'path') ||
88+
+ !this[STRIPABSOLUTEPATH](entry, 'linkpath')
89+
+ ) {
90+
+ return false
91+
}
92+
93+
if (path.isAbsolute(entry.path)) {
94+
--- /dev/null
95+
+++ b/test/ghsa-8qq5-rm4j-mr97.js
96+
@@ -0,0 +1,49 @@
97+
+const { readFileSync, readlinkSync, writeFileSync } = require('fs')
98+
+const { resolve } = require('path')
99+
+const t = require('tap')
100+
+const Header = require('../lib/header.js')
101+
+const x = require('../lib/extract.js')
102+
+
103+
+const targetSym = '/some/absolute/path'
104+
+
105+
+const getExploitTar = () => {
106+
+ const exploitTar = Buffer.alloc(512 + 512 + 1024)
107+
+
108+
+ new Header({
109+
+ path: 'exploit_hard',
110+
+ type: 'Link',
111+
+ size: 0,
112+
+ linkpath: resolve(t.testdirName, 'secret.txt'),
113+
+ }).encode(exploitTar, 0)
114+
+
115+
+ new Header({
116+
+ path: 'exploit_sym',
117+
+ type: 'SymbolicLink',
118+
+ size: 0,
119+
+ linkpath: targetSym,
120+
+ }).encode(exploitTar, 512)
121+
+
122+
+ return exploitTar
123+
+}
124+
+
125+
+const dir = t.testdir({
126+
+ 'secret.txt': 'ORIGINAL DATA',
127+
+ 'exploit.tar': getExploitTar(),
128+
+ out_repro: {},
129+
+})
130+
+
131+
+const out = resolve(dir, 'out_repro')
132+
+const tarFile = resolve(dir, 'exploit.tar')
133+
+
134+
+t.test('verify that linkpaths get sanitized properly', async t => {
135+
+ await x({
136+
+ cwd: out,
137+
+ file: tarFile,
138+
+ preservePaths: false,
139+
+ })
140+
+
141+
+ writeFileSync(resolve(out, 'exploit_hard'), 'OVERWRITTEN')
142+
+ t.equal(readFileSync(resolve(dir, 'secret.txt'), 'utf8'), 'ORIGINAL DATA')
143+
+
144+
+ t.not(readlinkSync(resolve(out, 'exploit_sym')), targetSym)
145+
+})
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
Description: normalize out unicode ligatures
2+
Author: Yadd <yadd@debian.org>
3+
Origin: upstream, https://github.com/isaacs/node-tar/commit/3b1abfae
4+
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-r6q2-hw4h-h46w
5+
Forwarded: not-needed
6+
Applied-Upstream: 7.5.4, commit:3b1abfae
7+
Reviewed-By: Xavier Guimard <yadd@debian.org>
8+
Last-Update: 2026-01-22
9+
10+
--- a/lib/normalize-unicode.js
11+
+++ b/lib/normalize-unicode.js
12+
@@ -6,7 +6,11 @@
13+
const { hasOwnProperty } = Object.prototype
14+
module.exports = s => {
15+
if (!hasOwnProperty.call(normalizeCache, s)) {
16+
- normalizeCache[s] = s.normalize('NFD')
17+
+ // shake out identical accents and ligatures
18+
+ normalizeCache[s] = s
19+
+ .normalize('NFD')
20+
+ .toLocaleLowerCase('en')
21+
+ .toLocaleUpperCase('en')
22+
}
23+
return normalizeCache[s]
24+
}
25+
--- a/lib/path-reservations.js
26+
+++ b/lib/path-reservations.js
27+
@@ -123,7 +123,7 @@
28+
// effectively removing all parallelization on windows.
29+
paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
30+
// don't need normPath, because we skip this entirely for windows
31+
- return stripSlashes(join(normalize(p))).toLowerCase()
32+
+ return stripSlashes(join(normalize(p)))
33+
})
34+
35+
const dirs = new Set(
36+
--- a/tap-snapshots/test/normalize-unicode.js.test.cjs
37+
+++ b/tap-snapshots/test/normalize-unicode.js.test.cjs
38+
@@ -6,25 +6,25 @@
39+
*/
40+
'use strict'
41+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "1/4foo.txt" > normalized 1`] = `
42+
-1/4foo.txt
43+
+1/4FOO.TXT
44+
`
45+
46+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "\\\\a\\\\b\\\\c\\\\d\\\\" > normalized 1`] = `
47+
-/a/b/c/d
48+
+/A/B/C/D
49+
`
50+
51+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "¼foo.txt" > normalized 1`] = `
52+
-¼foo.txt
53+
+¼FOO.TXT
54+
`
55+
56+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "﹨aaaa﹨dddd﹨" > normalized 1`] = `
57+
-﹨aaaa﹨dddd﹨
58+
+﹨AAAA﹨DDDD﹨
59+
`
60+
61+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "\bbb\eee\" > normalized 1`] = `
62+
-\bbb\eee\
63+
+\BBB\EEE\
64+
`
65+
66+
exports[`test/normalize-unicode.js TAP normalize with strip slashes "\\\\\eee\\\\\\" > normalized 1`] = `
67+
-\\\\\eee\\\\\\
68+
+\\\\\EEE\\\\\\
69+
`
70+
--- /dev/null
71+
+++ b/test/ghsa-r6q2-hw4h-h46w.js
72+
@@ -0,0 +1,49 @@
73+
+const t = require('tap')
74+
+const normalizeUnicode = require('../lib/normalize-unicode.js')
75+
+const Header = require('../lib/header.js')
76+
+const { resolve } = require('path')
77+
+const { lstatSync, readFileSync, statSync } = require('fs')
78+
+const extract = require('../lib/extract.js')
79+
+
80+
+// these characters are problems on macOS's APFS
81+
+const chars = {
82+
+ ['ff'.normalize('NFC')]: 'FF',
83+
+ ['fi'.normalize('NFC')]: 'FI',
84+
+ ['fl'.normalize('NFC')]: 'FL',
85+
+ ['ffi'.normalize('NFC')]: 'FFI',
86+
+ ['ffl'.normalize('NFC')]: 'FFL',
87+
+ ['ſt'.normalize('NFC')]: 'ST',
88+
+ ['st'.normalize('NFC')]: 'ST',
89+
+ ['ẛ'.normalize('NFC')]: 'Ṡ',
90+
+ ['ß'.normalize('NFC')]: 'SS',
91+
+ ['ẞ'.normalize('NFC')]: 'SS',
92+
+ ['ſ'.normalize('NFC')]: 'S',
93+
+}
94+
+
95+
+for (const [c, n] of Object.entries(chars)) {
96+
+ t.test(`${c} => ${n}`, async t => {
97+
+ t.equal(normalizeUnicode(c), n)
98+
+
99+
+ t.test('link then file', async t => {
100+
+ const tarball = Buffer.alloc(2048)
101+
+ new Header({
102+
+ path: c,
103+
+ type: 'SymbolicLink',
104+
+ linkpath: './target',
105+
+ }).encode(tarball, 0)
106+
+ new Header({
107+
+ path: n,
108+
+ type: 'File',
109+
+ size: 1,
110+
+ }).encode(tarball, 512)
111+
+ tarball[1024] = 'x'.charCodeAt(0)
112+
+
113+
+ const cwd = t.testdir({ tarball })
114+
+
115+
+ await extract({ cwd, file: resolve(cwd, 'tarball') })
116+
+
117+
+ t.throws(() => statSync(resolve(cwd, 'target')))
118+
+ t.equal(readFileSync(resolve(cwd, n), 'utf8'), 'x')
119+
+ })
120+
+ })
121+
+}
122+
--- a/test/normalize-unicode.js
123+
+++ b/test/normalize-unicode.js
124+
@@ -12,7 +12,7 @@
125+
126+
t.equal(normalize(cafe1), normalize(cafe2), 'matching unicodes')
127+
t.equal(normalize(cafe1), normalize(cafe2), 'cached')
128+
-t.equal(normalize('foo'), 'foo', 'non-unicode string')
129+
+t.equal(normalize('foo'), 'FOO', 'non-unicode string')
130+
131+
t.test('normalize with strip slashes', t => {
132+
const paths = [
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Description: properly sanitize hard links containing ..
2+
The issue is that *hard* links are resolved relative to the unpack cwd,
3+
so if they have `..`, they cannot possibly be valid. The loosening of
4+
the '..' restriction for symbolic links should have been limited by type.
5+
Author: isaacs <i@izs.me>
6+
Origin: upstream, https://github.com/isaacs/node-tar/commit/f4a7aa9b
7+
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-34x7-hfp2-rc4v
8+
Forwarded: not-needed
9+
Applied-Upstream: 7.5.7, commit:f4a7aa9b
10+
Last-Update: 2026-03-24
11+
12+
--- a/lib/unpack.js
13+
+++ b/lib/unpack.js
14+
@@ -251,11 +251,13 @@
15+
// return true if the field was successfully sanitized
16+
[STRIPABSOLUTEPATH]( entry, field ) {
17+
const path = entry[field]
18+
+ const { type } = entry
19+
if (!path || this.preservePaths) return true
20+
21+
const parts = path.split('/')
22+
if (
23+
- parts.includes('..') ||
24+
+ (parts.includes('..') &&
25+
+ (field === 'path' || type === 'Link')) ||
26+
/* c8 ignore next */
27+
(isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
28+
) {

0 commit comments

Comments
 (0)