[Pkg-javascript-devel] Bug#1131776: trixie-pu: package node-tar/6.2.1+~cs7.0.8-1+deb13u1
Xavier Guimard
yadd at debian.org
Tue Mar 24 11:41:28 GMT 2026
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: node-tar at packages.debian.org
Control: affects -1 + src:node-tar
User: release.debian.org at packages.debian.org
Usertags: pu
[ Reason ]
node-tar is vulnerable to 6 CVE. The more important is the possibility
to points to a file outside the extraction root, enabling arbitrary file
read and write as the extracting user.
- CVE-2026-23745: sanitize absolute linkpaths properly
- CVE-2026-23950: normalize out unicode ligatures
- CVE-2026-29786: parse root off paths before sanitizing parts
- CVE-2026-26960: do not write linkpaths through symlinks
(Closes: #1129378)
- CVE-2026-24842: properly sanitize hard links containing '..'
- CVE-2026-31802: prevent escaping symlinks with drive-relative paths
The 2 lasts are regressions introduced by CVE-2026-23745 patch
[ Impact ]
Medium security issues
[ Tests ]
Test pass
[ Risks ]
Medium risk, test pass and test coverage looks good
[ Checklist ]
[X] *all* changes are documented in the d/changelog
[X] I reviewed all changes and I approve them
[X] attach debdiff against the package in (old)stable
[X] the issue is verified as fixed in unstable
Best regards,
Xavier
-------------- next part --------------
diff --git a/debian/changelog b/debian/changelog
index 32e118b..968ebc5 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+node-tar (6.2.1+~cs7.0.8-1+deb13u1) trixie; urgency=medium
+
+ * Team upload
+ * Add patches for 6 CVEs: CVE-2026-23745, CVE-2026-23950, CVE-2026-24842,
+ CVE-2026-26960, CVE-2026-29786, CVE-2026-31802 (Closes: #1129378)
+
+ -- Xavier Guimard <yadd at debian.org> Tue, 24 Mar 2026 12:34:05 +0100
+
node-tar (6.2.1+~cs7.0.8-1) unstable; urgency=medium
* New upstream version
diff --git a/debian/patches/CVE-2026-23745.patch b/debian/patches/CVE-2026-23745.patch
new file mode 100644
index 0000000..146fb83
--- /dev/null
+++ b/debian/patches/CVE-2026-23745.patch
@@ -0,0 +1,145 @@
+Description: sanitize absolute linkpaths properly
+Author: isaacs <i at 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 at 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)
++})
diff --git a/debian/patches/CVE-2026-23950.patch b/debian/patches/CVE-2026-23950.patch
new file mode 100644
index 0000000..ab164b4
--- /dev/null
+++ b/debian/patches/CVE-2026-23950.patch
@@ -0,0 +1,132 @@
+Description: normalize out unicode ligatures
+Author: Yadd <yadd at 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 at 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 = {
++ ['?'.normalize('NFC')]: 'FF',
++ ['?'.normalize('NFC')]: 'FI',
++ ['?'.normalize('NFC')]: 'FL',
++ ['?'.normalize('NFC')]: 'FFI',
++ ['?'.normalize('NFC')]: 'FFL',
++ ['?'.normalize('NFC')]: 'ST',
++ ['?'.normalize('NFC')]: 'ST',
++ ['??'.normalize('NFC')]: 'S?',
++ ['?'.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 = [
diff --git a/debian/patches/CVE-2026-24842.patch b/debian/patches/CVE-2026-24842.patch
new file mode 100644
index 0000000..f7f547b
--- /dev/null
+++ b/debian/patches/CVE-2026-24842.patch
@@ -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 at 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] ?? ''))
+ ) {
diff --git a/debian/patches/CVE-2026-26960.patch b/debian/patches/CVE-2026-26960.patch
new file mode 100644
index 0000000..de59193
--- /dev/null
+++ b/debian/patches/CVE-2026-26960.patch
@@ -0,0 +1,139 @@
+From: isaacs <i at izs.me>
+Date: Thu, 12 Feb 2026 20:50:19 -0800
+Subject: [PATCH] <short summary of the patch>
+Origin: upstream, https://github.com/isaacs/node-tar/commit/d18e4e1f
+Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-83g3-92jg-28cx
+Forwarded: not-needed
+Applied-Upstream: 7.5.7, commit:d18e4e1f
+Reviewed-By: Xavier Guimard <yadd at debian.org>
+
+--- /dev/null
++++ b/lib/process-umask.js
+@@ -0,0 +1,4 @@
++// separate file so I stop getting nagged in vim about deprecated API
++module.exports = {
++ umask: () => process.umask()
++};
+--- a/lib/unpack.js
++++ b/lib/unpack.js
+@@ -18,6 +18,7 @@
+ const normPath = require('./normalize-windows-path.js')
+ const stripSlash = require('./strip-trailing-slashes.js')
+ const normalize = require('./normalize-unicode.js')
++const { umask } = require('./process-umask.js')
+
+ const ONENTRY = Symbol('onEntry')
+ const CHECKFS = Symbol('checkFs')
+@@ -30,6 +31,7 @@
+ const LINK = Symbol('link')
+ const SYMLINK = Symbol('symlink')
+ const HARDLINK = Symbol('hardlink')
++const ENSURE_NO_SYMLINK = Symbol('ensureNoSymlink')
+ const UNSUPPORTED = Symbol('unsupported')
+ const CHECKPATH = Symbol('checkPath')
+ const STRIPABSOLUTEPATH = Symbol('stripAbsolutePath')
+@@ -217,7 +219,7 @@
+ this.cwd = normPath(path.resolve(opt.cwd || process.cwd()))
+ this.strip = +opt.strip || 0
+ // if we're not chmodding, then we don't need the process umask
+- this.processUmask = opt.noChmod ? 0 : process.umask()
++ this.processUmask = opt.noChmod ? 0 : umask()
+ this.umask = typeof opt.umask === 'number' ? opt.umask : this.processUmask
+
+ // default mode for dirs created as parents
+@@ -565,12 +567,64 @@
+ }
+
+ [SYMLINK] (entry, done) {
+- this[LINK](entry, entry.linkpath, 'symlink', done)
++ const parts = normPath(
++ path.relative(
++ this.cwd,
++ path.resolve(
++ path.dirname(String(entry.absolute)),
++ String(entry.linkpath),
++ ),
++ ),
++ ).split('/')
++ this[ENSURE_NO_SYMLINK](
++ entry,
++ this.cwd,
++ parts,
++ () =>
++ this[LINK](entry, String(entry.linkpath), 'symlink', done),
++ er => {
++ this[ONERROR](er, entry)
++ done()
++ },
++ )
+ }
+
+ [HARDLINK] (entry, done) {
+ const linkpath = normPath(path.resolve(this.cwd, entry.linkpath))
+- this[LINK](entry, linkpath, 'link', done)
++ const parts = normPath(String(entry.linkpath)).split(
++ '/',
++ )
++ this[ENSURE_NO_SYMLINK](
++ entry,
++ this.cwd,
++ parts,
++ () => this[LINK](entry, linkpath, 'link', done),
++ er => {
++ this[ONERROR](er, entry)
++ done()
++ },
++ )
++ }
++
++ [ENSURE_NO_SYMLINK](
++ entry,
++ cwd,
++ parts,
++ done,
++ onError,
++ ) {
++ const p = parts.shift()
++ if (this.preservePaths || p === undefined) return done()
++ const t = path.resolve(cwd, p)
++ fs.lstat(t, (er, st) => {
++ if (er) return done()
++ if (st?.isSymbolicLink()) {
++ return onError(
++ new SymlinkError(t, path.resolve(t, parts.join('/'))),
++ )
++ }
++ this[ENSURE_NO_SYMLINK](entry, t, parts, done, onError)
++ })
+ }
+
+ [PEND] () {
+@@ -935,6 +989,28 @@
+ }
+ }
+
++ [ENSURE_NO_SYMLINK](
++ _entry,
++ cwd,
++ parts,
++ done,
++ onError,
++ ) {
++ if (this.preservePaths || !parts.length) return done()
++ let t = cwd
++ for (const p of parts) {
++ t = path.resolve(t, p)
++ const [er, st] = callSync(() => fs.lstatSync(t))
++ if (er) return done()
++ if (st.isSymbolicLink()) {
++ return onError(
++ new SymlinkError(t, path.resolve(cwd, parts.join('/'))),
++ )
++ }
++ }
++ done()
++ }
++
+ [LINK] (entry, linkpath, link, done) {
+ try {
+ fs[link + 'Sync'](linkpath, entry.absolute)
diff --git a/debian/patches/CVE-2026-29786.patch b/debian/patches/CVE-2026-29786.patch
new file mode 100644
index 0000000..c261d51
--- /dev/null
+++ b/debian/patches/CVE-2026-29786.patch
@@ -0,0 +1,22 @@
+From: isaacs <i at izs.me>
+Date: Wed, 4 Mar 2026 11:41:10 -0800
+Subject: [PATCH] parse root off paths before sanitizing .. parts
+Origin: upstream, https://github.com/isaacs/node-tar/commit/7bc755dd
+Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-qffp-2rhf-9h96
+Forwarded: not-needed
+Applied-Upstream: 7.5.10, commit:7bc755dd
+Reviewed-By: Xavier Guimard <yadd at debian.org>
+
+--- a/lib/unpack.js
++++ b/lib/unpack.js
+@@ -284,7 +284,9 @@
+
+ [CHECKPATH] (entry) {
+ const p = normPath(entry.path)
+- const parts = p.split('/')
++ // strip off the root
++ const [root, stripped] = stripAbsolutePath(p)
++ const parts = stripped.replace(/\\/g, '/').split('/')
+
+ if (this.strip) {
+ if (parts.length < this.strip) {
diff --git a/debian/patches/CVE-2026-31802.patch b/debian/patches/CVE-2026-31802.patch
new file mode 100644
index 0000000..d9aa7dc
--- /dev/null
+++ b/debian/patches/CVE-2026-31802.patch
@@ -0,0 +1,33 @@
+Description: prevent escaping symlinks with drive-relative paths
+ After stripping the drive letter root from paths like c:../../../foo,
+ re-check for '..' to prevent path traversal via drive-relative linkpaths.
+Author: isaacs <i at izs.me>
+Origin: upstream, https://github.com/isaacs/node-tar/commit/f48b5fa3
+Bug: https://github.com/isaacs/node-tar/security/advisories/GHSA-9ppj-qmqm-q256
+Forwarded: not-needed
+Applied-Upstream: 7.5.11, commit:f48b5fa3
+Last-Update: 2026-03-24
+
+--- a/lib/unpack.js
++++ b/lib/unpack.js
+@@ -272,6 +272,20 @@
+ // strip off the root
+ const [root, stripped] = stripAbsolutePath(path)
+ if (root) {
++ // After stripping root, re-check for '..' in the stripped path
++ // This catches drive-relative paths like c:../../../foo where
++ // the initial check missed '..' because it was part of 'c:..'
++ const strippedParts = String(stripped).replace(/\\/g, '/').split('/')
++ if (
++ strippedParts.includes('..') &&
++ (field === 'path' || entry.type === 'Link')
++ ) {
++ this.warn('TAR_ENTRY_ERROR', `linkpath escapes extraction directory`, {
++ entry,
++ [field]: path,
++ })
++ return false
++ }
+ // ok, but triggers warning about stripping root
+ entry[field] = String(stripped)
+ this.warn(
diff --git a/debian/patches/series b/debian/patches/series
index b52771a..2eda2d6 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1 +1,7 @@
api-backward-compatibility.patch
+CVE-2026-23745.patch
+CVE-2026-23950.patch
+CVE-2026-29786.patch
+CVE-2026-26960.patch
+CVE-2026-24842.patch
+CVE-2026-31802.patch
More information about the Pkg-javascript-devel
mailing list