← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2026-45736 · CWE-908 · Disclosed 2026-05-15

ws is an open source WebSocket client and server for Node

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is less a broken front door than a note slot that leaks paper only if your own code stuffs it wrong

ws before 8.20.1 can disclose uninitialized process memory when application code calls websocket.close() with a TypedArray as the reason argument. The published affected upstream range is >= 8.0.0 < 8.20.1; Debian tracks affected node-ws packages in bullseye, bookworm, and trixie, while Ubuntu had not yet published package-level fix status across supported releases at the time of review.

The vendor's MEDIUM 4.4 is still a little generous for enterprise patch triage. The decisive friction is that exploitation is not a normal unauthenticated network path; it depends on *application misuse* or an attacker already having enough influence over application logic to make the process pass a TypedArray into close(), which is why even the upstream GitHub advisory remarks that real-world severity is believed to be low.

"Real bug, but it mostly requires the victim app to misuse `ws.close()` in a very specific way."
02 · The Attack Path

3 steps from start to impact.

STEP 01

Reach a ws-backed code path

The attacker first has to interact with an application that actually uses the ws library in a reachable server or client workflow. This is already narrower than a product CVE because ws is a dependency, not a directly fingerprintable exposed service with a stable banner. Weaponized tooling here is usually just a bespoke PoC using Node.js and ws itself, as shown in the GitHub advisory.
Conditions required:
  • A target application includes upstream ws in the vulnerable range >=8.0.0 <8.20.1
  • The attacker can reach the relevant WebSocket workflow over the network or influence a connected peer
Where this breaks in practice:
  • Internet scanners do not reliably fingerprint npm dependency versions
  • Many deployments use ws transitively and never expose a path where close-frame reasons are attacker-influenced
Detection/coverage: SCA/SBOM tooling will detect vulnerable package versions; perimeter scanners generally will not.
STEP 02

Force application misuse of websocket.close()

The real exploit hinge is getting the victim process to call websocket.close(code, reason) with a TypedArray rather than a string or Buffer. Per the fix commit, the bug exists because the function used Buffer.allocUnsafe() and failed to overwrite that buffer correctly for this argument type, which can leak dirty memory to the remote peer. In practice this usually means a buggy wrapper, custom middleware, unsafe plugin, or some already-compromised code path that forwards attacker-controlled objects into the close reason.
Conditions required:
  • Application logic passes a TypedArray into close()
  • The attacker can influence that argument or the code path invoking it
Where this breaks in practice:
  • This is not the normal API usage pattern for close reasons
  • Requiring attacker control over an internal object type is effectively post-bug or post-compromise in most real applications
  • Type validation, framework wrappers, or serialization layers often collapse values to strings before this point
Detection/coverage: Code review, SAST, and targeted unit tests are better than runtime network signatures here.
STEP 03

Receive leaked heap bytes in the close frame

If the vulnerable call is made, the remote peer can receive uninitialized memory in the close reason payload and potentially recover sensitive bytes from the Node.js process heap. The impact is confidentiality-only; there is no integrity or availability effect in the published advisory, and no evidence this becomes code execution by itself.
Conditions required:
  • A vulnerable close() invocation occurs
  • The attacker controls or can observe the remote WebSocket peer receiving the close frame
Where this breaks in practice:
  • Close reason payload size is small, so exfiltration is constrained
  • Useful secrets must happen to be resident in adjacent process memory at the right moment
  • Even successful leakage may produce low-value garbage rather than credentials or tokens
Detection/coverage: Network IDS is unlikely to spot this cleanly; app-layer logging of abnormal close reasons and packet capture during repro are more realistic.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo public evidence of active exploitation found during this review, and it is not known KEV-listed.
Proof of conceptYes. GitHub advisory GHSA-58qx-3vcg-4xpx includes a working Node.js PoC using WebSocketServer and Float32Array(20).
EPSS0.00012 from the user-supplied intel — effectively near-floor exploit likelihood.
KEV statusNot listed in the CISA KEV catalog during this review; Cyber Trackr's public mirror of KEV had no entry for this CVE and reported catalog currency through 2026-05-22.
CVSS vectorVendor/CNA vector is CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:N/A:N, which already encodes substantial friction: *high attack complexity* and *high privileges required* for a confidentiality-only outcome.
Score disagreementNVD added a 7.5 HIGH ADP score (AV:N/AC:L/PR:N/...) that does not match the CNA assessment. Based on the advisory and fix notes, the CNA score is closer to reality because exploitation depends on a very specific misuse pattern.
Affected versionsUpstream affected range is >= 8.0.0 < 8.20.1.
Fixed versionsUpstream fix is 8.20.1. Debian marks node-ws 8.20.1+~cs14.19.1-1 fixed in unstable/sid, while bookworm/trixie/bullseye were still tracked as vulnerable with minor-issue notes at review time.
Exposure visibilityPoor internet visibility. This is a library flaw inside Node.js applications, so Shodan/Censys/FOFA-style exposure counts are not meaningful for version-level targeting; package inventory and SBOM data matter more than external attack-surface scans.
Disclosure and creditPublished by GitHub on 2026-05-12 in the advisory, with CVE/NVD publication on 2026-05-15. Credit is given to Nikita Skovoroda / ChALkeR.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (2.3/10)

The single biggest downgrade driver is that exploitation depends on *the victim application misusing the API* by passing a TypedArray into websocket.close(), not on a broadly reachable pre-auth network bug. That makes this a narrow, post-logic flaw with weak attacker economics even in very large Node.js estates.

HIGH Exploitability is materially lower than generic network-facing CVEs
MEDIUM Enterprise prevalence of vulnerable package versions may still be high because `ws` is widely depended on

Why this verdict

  • Downgrade for prerequisite stack: attacker needs a vulnerable ws build *and* a reachable code path *and* a way to make the app call close() with a TypedArray, which compounds friction hard.
  • Downgrade for blast radius: impact is confidentiality-only and the leak is bounded by a close-frame path, not a general read primitive or code execution path.
  • Downgrade for threat signal: no KEV listing, no public exploitation evidence found, and the supplied EPSS is extremely low.
  • Keep it above IGNORE: the bug is real, has a published PoC, and can leak process memory if your application or wrapper actually hits the bad call pattern.

Why not higher?

This is not a clean unauthenticated remote exploit against the exposed service layer. The exploit chain relies on unusual application behavior and often implies the attacker already has control over a higher-level logic path or plugin/custom code that feeds the bad argument type.

Why not lower?

It is still a genuine memory disclosure bug with a vendor patch, not a purely theoretical parser edge case. In environments with custom WebSocket wrappers, multi-tenant app logic, or plugins that reflect structured data into close reasons, the misuse condition can exist accidentally.

05 · Compensating Control

What to do — in priority order.

  1. Inventory ws versions from SBOMs and lockfiles — Find every direct and transitive ws dependency first; this is the only reliable way to scope exposure because network scanners cannot fingerprint it. For a LOW verdict there is no formal mitigation SLA, so treat this as backlog hygiene and complete the inventory during the next normal dependency review cycle.
  2. Review wrappers around websocket.close() — Search custom code for any wrapper or helper that forwards non-string objects into the close reason argument, especially typed arrays or binary views. For a LOW verdict there is no formal mitigation SLA, so roll this into routine secure-code review rather than emergency change control.
  3. Enforce type validation at call sites — If you cannot upgrade immediately, clamp close reasons to strings or Buffer objects before they reach ws, which removes the published trigger condition. For a LOW verdict there is no formal mitigation SLA, so apply this as part of ordinary hardening work where the package is embedded in longer-lived applications.
  4. Prioritize internet-facing multi-tenant Node services first — If you must choose, start with externally reachable apps where tenants or plugins can influence WebSocket session teardown behavior, because that's where the misuse condition is most plausible. For a LOW verdict there is no formal mitigation SLA, so sequence this within normal patch backlog handling.
What doesn't work
  • WAF signatures do not help much because the vulnerable condition is an internal API misuse, not a stable HTTP payload pattern.
  • Edge network scanning will not reliably tell you whether you're running a vulnerable ws version, because this is a library dependency rather than a bannered product.
  • MFA or SSO controls are mostly irrelevant unless your application bug already requires authenticated misuse; they do not remove the unsafe close() behavior itself.
06 · Verification

Crowdsourced verification payload.

Run this on the target host, container image, build workspace, or CI runner anywhere your Node.js application files are present. Invoke it with python3 check_ws_cve_2026_45736.py /path/to/app and no elevated privileges are normally required; it walks the directory tree, finds installed node_modules/**/ws/package.json files, and reports VULNERABLE, PATCHED, or UNKNOWN.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# CVE-2026-45736 verifier for installed ws packages
# Usage: python3 check_ws_cve_2026_45736.py /path/to/app
# Exit codes:
#   0 = PATCHED
#   1 = VULNERABLE
#   2 = UNKNOWN / no determination

import json
import os
import re
import sys
from typing import List, Optional, Tuple

VULN_MIN = (8, 0, 0)
FIXED = (8, 20, 1)


def parse_semver(version: str) -> Optional[Tuple[int, int, int, str]]:
    if not version or not isinstance(version, str):
        return None
    m = re.match(r'^\s*v?(\d+)\.(\d+)\.(\d+)(.*)$', version.strip())
    if not m:
        return None
    return (int(m.group(1)), int(m.group(2)), int(m.group(3)), m.group(4) or '')


def is_vulnerable(version: str) -> Optional[bool]:
    parsed = parse_semver(version)
    if parsed is None:
        return None
    core = parsed[:3]
    return core >= VULN_MIN and core < FIXED


def find_ws_packages(root: str) -> List[Tuple[str, str]]:
    results = []
    for dirpath, dirnames, filenames in os.walk(root):
        # Skip very large or irrelevant trees when possible
        base = os.path.basename(dirpath)
        if base in {'.git', '.hg', '.svn', '__pycache__'}:
            dirnames[:] = []
            continue

        if 'package.json' not in filenames:
            continue

        norm = dirpath.replace('\\', '/')
        if '/node_modules/ws' not in norm:
            continue

        pkg_json = os.path.join(dirpath, 'package.json')
        try:
            with open(pkg_json, 'r', encoding='utf-8') as f:
                data = json.load(f)
            if data.get('name') == 'ws':
                version = str(data.get('version', '')).strip()
                results.append((pkg_json, version))
        except Exception:
            results.append((pkg_json, ''))
    return results


def main() -> int:
    if len(sys.argv) != 2:
        print('UNKNOWN: usage: python3 check_ws_cve_2026_45736.py /path/to/app')
        return 2

    root = sys.argv[1]
    if not os.path.exists(root):
        print(f'UNKNOWN: path does not exist: {root}')
        return 2

    findings = find_ws_packages(root)
    if not findings:
        print('UNKNOWN: no installed ws package instances found under node_modules')
        return 2

    vulnerable = []
    patched = []
    unknown = []

    for path, version in findings:
        verdict = is_vulnerable(version)
        if verdict is True:
            vulnerable.append((path, version))
        elif verdict is False:
            patched.append((path, version))
        else:
            unknown.append((path, version))

    if vulnerable:
        print('VULNERABLE: found ws versions in affected range >=8.0.0 <8.20.1')
        for path, version in vulnerable:
            print(f'  - {version or "unknown-version"} @ {path}')
        if patched:
            print('Also found patched/non-vulnerable instances:')
            for path, version in patched:
                print(f'  - {version or "unknown-version"} @ {path}')
        if unknown:
            print('Could not parse these instances:')
            for path, version in unknown:
                print(f'  - {version or "unknown-version"} @ {path}')
        return 1

    if patched and not unknown:
        print('PATCHED: all discovered ws instances are outside the vulnerable range or at/above 8.20.1')
        for path, version in patched:
            print(f'  - {version or "unknown-version"} @ {path}')
        return 0

    print('UNKNOWN: no vulnerable versions found, but one or more ws instances could not be parsed')
    if patched:
        print('Parsed patched/non-vulnerable instances:')
        for path, version in patched:
            print(f'  - {version or "unknown-version"} @ {path}')
    if unknown:
        print('Unparsed instances:')
        for path, version in unknown:
            print(f'  - {version or "unknown-version"} @ {path}')
    return 2


if __name__ == '__main__':
    sys.exit(main())
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: do not burn emergency patch capacity on this unless your own code review shows you pass typed arrays into websocket.close() or you run highly customized multi-tenant WebSocket services. For a LOW noisgate verdict there is no noisgate mitigation SLA and noisgate remediation SLA is no SLA (treat as backlog hygiene), so go straight to normal dependency management: inventory where ws is present this week, prioritize exposed custom apps, and fold upgrades to 8.20.1+ into the next regular maintenance sprint rather than creating an out-of-band patch event.

Sources

  1. NVD CVE-2026-45736
  2. GitHub Security Advisory GHSA-58qx-3vcg-4xpx
  3. Upstream fix commit c0327ec
  4. GitLab Advisory Database entry
  5. Debian security tracker
  6. Ubuntu security notice
  7. CISA Known Exploited Vulnerabilities Catalog
  8. FIRST EPSS API documentation
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.