Your message dated Sat, 16 May 2026 10:23:18 +0000
with message-id <[email protected]>
and subject line Released with 13.5
has caused the Debian Bug report #1131776,
regarding trixie-pu: package node-tar/6.2.1+~cs7.0.8-1+deb13u1
to be marked as done.

This means that you claim that the problem has been dealt with.
If this is not the case it is now your responsibility to reopen the
Bug report if necessary, and/or fix the problem forthwith.

(NB: If you are a system administrator and have no idea what this
message is talking about, this may indicate a serious mail system
misconfiguration somewhere. Please contact [email protected]
immediately.)


-- 
1131776: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1131776
Debian Bug Tracking System
Contact [email protected] with problems
--- Begin Message ---
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected]
Control: affects -1 + src:node-tar
User: [email protected]
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
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 <[email protected]>  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 <[email protected]>
+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 <[email protected]>
+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 <[email protected]>
+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 <[email protected]>
+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 = [
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 <[email protected]>
+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 <[email protected]>
+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 <[email protected]>
+
+--- /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 <[email protected]>
+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 <[email protected]>
+
+--- 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 <[email protected]>
+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

--- End Message ---
--- Begin Message ---
Package: release.debian.org
Version: 13.5

This update has been released as part of Debian 13.5.

--- End Message ---

Reply via email to