Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
node-tar (6.2.1+~cs7.0.8-1deepin1) unstable; urgency=medium

* Fix CVE-2026-23745: Insufficient Link Path Sanitization
* Fix CVE-2026-23950: Race Condition in node-tar Path Reservations
* Fix CVE-2026-29786: Hardlink Path Traversal via Drive-Relative
Linkpath
* Fix CVE-2026-26960: Arbitrary File Read/Write via Hardlink Target
Escape
* Fix CVE-2026-24842: Arbitrary File Read/Overwrite via Hardlink Path
Traversal
* Fix CVE-2026-31802: Symlink Path Traversal via Drive-Relative
Linkpath
* Adapt to chownr 3 (named exports)

-- deepin-ci-robot <packages@deepin.org> Fri, 17 Apr 2026 02:23:36 +0800

node-tar (6.2.1+~cs7.0.8-1) unstable; urgency=medium

* New upstream version
Expand Down
145 changes: 145 additions & 0 deletions debian/patches/CVE-2026-23745.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
Description: sanitize absolute linkpaths properly
Author: isaacs <i@izs.me>
Origin: upstream, https://github.com/isaacs/node-tar/commit/340eb285
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-8qq5-rm4j-mr97
Forwarded: not-needed
Applied-Upstream: 7.5.3, commit:340eb285
Reviewed-By: Xavier Guimard <yadd@debian.org>
Last-Update: 2026-01-17

--- a/lib/unpack.js
+++ b/lib/unpack.js
@@ -32,6 +32,7 @@
const HARDLINK = Symbol('hardlink')
const UNSUPPORTED = Symbol('unsupported')
const CHECKPATH = Symbol('checkPath')
+const STRIPABSOLUTEPATH = Symbol('stripAbsolutePath')
const MKDIR = Symbol('mkdir')
const ONERROR = Symbol('onError')
const PENDING = Symbol('pending')
@@ -244,6 +245,43 @@
}
}

+ // return false if we need to skip this file
+ // return true if the field was successfully sanitized
+ [STRIPABSOLUTEPATH]( entry, field ) {
+ const path = entry[field]
+ if (!path || this.preservePaths) return true
+
+ const parts = path.split('/')
+ if (
+ parts.includes('..') ||
+ /* c8 ignore next */
+ (isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
+ ) {
+ this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
+ entry,
+ [field]: path,
+ })
+ // not ok!
+ return false
+ }
+
+ // strip off the root
+ const [root, stripped] = stripAbsolutePath(path)
+ if (root) {
+ // ok, but triggers warning about stripping root
+ entry[field] = String(stripped)
+ this.warn(
+ 'TAR_ENTRY_INFO',
+ `stripping ${root} from absolute ${field}`,
+ {
+ entry,
+ [field]: path,
+ },
+ )
+ }
+ return true
+ }
+
[CHECKPATH] (entry) {
const p = normPath(entry.path)
const parts = p.split('/')
@@ -274,24 +312,11 @@
return false
}

- if (!this.preservePaths) {
- if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) {
- this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
- entry,
- path: p,
- })
- return false
- }
-
- // strip off the root
- const [root, stripped] = stripAbsolutePath(p)
- if (root) {
- entry.path = stripped
- this.warn('TAR_ENTRY_INFO', `stripping ${root} from absolute path`, {
- entry,
- path: p,
- })
- }
+ if (
+ !this[STRIPABSOLUTEPATH](entry, 'path') ||
+ !this[STRIPABSOLUTEPATH](entry, 'linkpath')
+ ) {
+ return false
}

if (path.isAbsolute(entry.path)) {
--- /dev/null
+++ b/test/ghsa-8qq5-rm4j-mr97.js
@@ -0,0 +1,49 @@
+const { readFileSync, readlinkSync, writeFileSync } = require('fs')
+const { resolve } = require('path')
+const t = require('tap')
+const Header = require('../lib/header.js')
+const x = require('../lib/extract.js')
+
+const targetSym = '/some/absolute/path'
+
+const getExploitTar = () => {
+ const exploitTar = Buffer.alloc(512 + 512 + 1024)
+
+ new Header({
+ path: 'exploit_hard',
+ type: 'Link',
+ size: 0,
+ linkpath: resolve(t.testdirName, 'secret.txt'),
+ }).encode(exploitTar, 0)
+
+ new Header({
+ path: 'exploit_sym',
+ type: 'SymbolicLink',
+ size: 0,
+ linkpath: targetSym,
+ }).encode(exploitTar, 512)
+
+ return exploitTar
+}
+
+const dir = t.testdir({
+ 'secret.txt': 'ORIGINAL DATA',
+ 'exploit.tar': getExploitTar(),
+ out_repro: {},
+})
+
+const out = resolve(dir, 'out_repro')
+const tarFile = resolve(dir, 'exploit.tar')
+
+t.test('verify that linkpaths get sanitized properly', async t => {
+ await x({
+ cwd: out,
+ file: tarFile,
+ preservePaths: false,
+ })
+
+ writeFileSync(resolve(out, 'exploit_hard'), 'OVERWRITTEN')
+ t.equal(readFileSync(resolve(dir, 'secret.txt'), 'utf8'), 'ORIGINAL DATA')
+
+ t.not(readlinkSync(resolve(out, 'exploit_sym')), targetSym)
+})
132 changes: 132 additions & 0 deletions debian/patches/CVE-2026-23950.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
Description: normalize out unicode ligatures
Author: Yadd <yadd@debian.org>
Origin: upstream, https://github.com/isaacs/node-tar/commit/3b1abfae
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-r6q2-hw4h-h46w
Forwarded: not-needed
Applied-Upstream: 7.5.4, commit:3b1abfae
Reviewed-By: Xavier Guimard <yadd@debian.org>
Last-Update: 2026-01-22

--- a/lib/normalize-unicode.js
+++ b/lib/normalize-unicode.js
@@ -6,7 +6,11 @@
const { hasOwnProperty } = Object.prototype
module.exports = s => {
if (!hasOwnProperty.call(normalizeCache, s)) {
- normalizeCache[s] = s.normalize('NFD')
+ // shake out identical accents and ligatures
+ normalizeCache[s] = s
+ .normalize('NFD')
+ .toLocaleLowerCase('en')
+ .toLocaleUpperCase('en')
}
return normalizeCache[s]
}
--- a/lib/path-reservations.js
+++ b/lib/path-reservations.js
@@ -123,7 +123,7 @@
// effectively removing all parallelization on windows.
paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => {
// don't need normPath, because we skip this entirely for windows
- return stripSlashes(join(normalize(p))).toLowerCase()
+ return stripSlashes(join(normalize(p)))
})

const dirs = new Set(
--- a/tap-snapshots/test/normalize-unicode.js.test.cjs
+++ b/tap-snapshots/test/normalize-unicode.js.test.cjs
@@ -6,25 +6,25 @@
*/
'use strict'
exports[`test/normalize-unicode.js TAP normalize with strip slashes "1/4foo.txt" > normalized 1`] = `
-1/4foo.txt
+1/4FOO.TXT
`

exports[`test/normalize-unicode.js TAP normalize with strip slashes "\\\\a\\\\b\\\\c\\\\d\\\\" > normalized 1`] = `
-/a/b/c/d
+/A/B/C/D
`

exports[`test/normalize-unicode.js TAP normalize with strip slashes "¼foo.txt" > normalized 1`] = `
-¼foo.txt
+¼FOO.TXT
`

exports[`test/normalize-unicode.js TAP normalize with strip slashes "﹨aaaa﹨dddd﹨" > normalized 1`] = `
-﹨aaaa﹨dddd﹨
+﹨AAAA﹨DDDD﹨
`

exports[`test/normalize-unicode.js TAP normalize with strip slashes "\bbb\eee\" > normalized 1`] = `
-\bbb\eee\
+\BBB\EEE\
`

exports[`test/normalize-unicode.js TAP normalize with strip slashes "\\\\\eee\\\\\\" > normalized 1`] = `
-\\\\\eee\\\\\\
+\\\\\EEE\\\\\\
`
--- /dev/null
+++ b/test/ghsa-r6q2-hw4h-h46w.js
@@ -0,0 +1,49 @@
+const t = require('tap')
+const normalizeUnicode = require('../lib/normalize-unicode.js')
+const Header = require('../lib/header.js')
+const { resolve } = require('path')
+const { lstatSync, readFileSync, statSync } = require('fs')
+const extract = require('../lib/extract.js')
+
+// these characters are problems on macOS's APFS
+const chars = {
+ ['ff'.normalize('NFC')]: 'FF',
+ ['fi'.normalize('NFC')]: 'FI',
+ ['fl'.normalize('NFC')]: 'FL',
+ ['ffi'.normalize('NFC')]: 'FFI',
+ ['ffl'.normalize('NFC')]: 'FFL',
+ ['ſt'.normalize('NFC')]: 'ST',
+ ['st'.normalize('NFC')]: 'ST',
+ ['ẛ'.normalize('NFC')]: 'Ṡ',
+ ['ß'.normalize('NFC')]: 'SS',
+ ['ẞ'.normalize('NFC')]: 'SS',
+ ['ſ'.normalize('NFC')]: 'S',
+}
+
+for (const [c, n] of Object.entries(chars)) {
+ t.test(`${c} => ${n}`, async t => {
+ t.equal(normalizeUnicode(c), n)
+
+ t.test('link then file', async t => {
+ const tarball = Buffer.alloc(2048)
+ new Header({
+ path: c,
+ type: 'SymbolicLink',
+ linkpath: './target',
+ }).encode(tarball, 0)
+ new Header({
+ path: n,
+ type: 'File',
+ size: 1,
+ }).encode(tarball, 512)
+ tarball[1024] = 'x'.charCodeAt(0)
+
+ const cwd = t.testdir({ tarball })
+
+ await extract({ cwd, file: resolve(cwd, 'tarball') })
+
+ t.throws(() => statSync(resolve(cwd, 'target')))
+ t.equal(readFileSync(resolve(cwd, n), 'utf8'), 'x')
+ })
+ })
+}
--- a/test/normalize-unicode.js
+++ b/test/normalize-unicode.js
@@ -12,7 +12,7 @@

t.equal(normalize(cafe1), normalize(cafe2), 'matching unicodes')
t.equal(normalize(cafe1), normalize(cafe2), 'cached')
-t.equal(normalize('foo'), 'foo', 'non-unicode string')
+t.equal(normalize('foo'), 'FOO', 'non-unicode string')

t.test('normalize with strip slashes', t => {
const paths = [
28 changes: 28 additions & 0 deletions debian/patches/CVE-2026-24842.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Description: properly sanitize hard links containing ..
The issue is that *hard* links are resolved relative to the unpack cwd,
so if they have `..`, they cannot possibly be valid. The loosening of
the '..' restriction for symbolic links should have been limited by type.
Author: isaacs <i@izs.me>
Origin: upstream, https://github.com/isaacs/node-tar/commit/f4a7aa9b
Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-34x7-hfp2-rc4v
Forwarded: not-needed
Applied-Upstream: 7.5.7, commit:f4a7aa9b
Last-Update: 2026-03-24

--- a/lib/unpack.js
+++ b/lib/unpack.js
@@ -251,11 +251,13 @@
// return true if the field was successfully sanitized
[STRIPABSOLUTEPATH]( entry, field ) {
const path = entry[field]
+ const { type } = entry
if (!path || this.preservePaths) return true

const parts = path.split('/')
if (
- parts.includes('..') ||
+ (parts.includes('..') &&
+ (field === 'path' || type === 'Link')) ||
/* c8 ignore next */
(isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
) {
Loading
Loading