Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

GHSA-588r-cr5c-w6hf openexr

HIGH

GHSA-588r-cr5c-w6hf is a high-severity (CVSS 7.8) Out-of-bounds Read vulnerability in openexr. A fix is available for openexr — see the affected versions and patch details below.

OpenEXR has a signed 32-bit Overflow in PIZ Decoder Leads to OOB Read/Write

Also known asCVE-2026-34588PYSEC-2026-2845
Published
Apr 8, 2026
Updated
Jul 13, 2026
Affected
3 pkgs
Patched
3 / 3
Exploits
None indexed
Exploitation data as of Sep 19, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • A successful exploit gives an attacker total control of the affected component, not partial access.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-588r-cr5c-w6hf.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs41th percentile — riskier than 41% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-588r-cr5c-w6hf plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

3 pkgs affected
🐍openexr🐍openexr🐍openexr

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

internal_exr_undo_piz() advances the working wavelet pointer with signed 32-bit arithmetic:

wavbuf += nx * ny * wcount;

Because nx, ny, and wcount are int, a crafted EXR file can make this product overflow and wrap. The next channel then decodes from an incorrect address. The wavelet decode path operates in place, so this yields both out-of-bounds reads and out-of-bounds writes.

Tested on commit 7820b7e1b93405ba1d551c43a945018226b75bc5

Technical Details

The vulnerable decode path is:

  1. internal_exr_undo_piz() sets wavbuf = decode->scratch_buffer_1.
  2. For each channel, it calls wav_2D_decode (wavbuf + j, ...).
  3. It then advances wavbuf with wavbuf += nx * ny * wcount.

The overflow happens in step 3. Once wavbuf is wrapped, the next channel's wavelet decode runs on the wrong address.

In the 14-bit wavelet path, wdec14_4() first reads:

  • *px
  • *p10
  • *p01
  • *p11

and then writes back to the same locations:

  • *px = ...
  • *p01 = ...
  • *p10 = ...
  • *p11 = ...

As a result, the bug is not just a crash-only invalid read. It is an out-of-bounds read/write condition.

Reproduction

piz_scanline_redzone.zip

Build exrcheck with ASAN and run:

❯ ./build-asan/bin/exrcheck /tmp/piz_scanline_redzone.exr
 file /tmp/piz_scanline_redzone.exr /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:373:19: runtime error: signed integer overflow: 134217724 * 32 cannot be represented in type 'int'
=================================================================
==1711239==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7bedc3934700 at pc 0x7bf1f100f498 bp 0x7ffe032d8f00 sp 0x7ffe032d8ef0
READ of size 2 at 0x7bedc3934700 thread T0
    #0 0x7bf1f100f497 in wdec14_4 /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:148
    #1 0x7bf1f100f497 in wav_2D_decode /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:403
    #2 0x7bf1f100f497 in internal_exr_undo_piz /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:727
    #3 0x7bf1f115b038 in exr_uncompress_chunk /home/pop/sec/openexr/src/lib/OpenEXRCore/compression.c:546
    #4 0x7bf1f1161168 in exr_decoding_run /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:580
    #5 0x7bf1f2a71add in run_decode /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:586
    #6 0x7bf1f2a83dc4 in Imf_4_0::ScanLineInputFile::Data::readPixels(Imf_4_0::FrameBuffer const&, int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:500
    #7 0x7bf1f28c6a81 in Imf_4_0::InputFile::Data::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfInputFile.cpp:458
    #8 0x7bf1f3bfe2dc in readScanline<Imf_4_0::InputPart> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:239
    #9 0x7bf1f3c05b04 in readMultiPart /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:905
    #10 0x7bf1f3c126fd in runChecks<char const*> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1171
    #11 0x7bf1f3c146b9 in Imf_4_0::checkOpenEXRFile(char const*, bool, bool, bool) /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1835
    #12 0x5d9675fce8f8 in exrCheck(char const*, bool, bool, bool, bool) /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:96
    #13 0x5d9675fcb2b1 in main /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:164
    #14 0x7bf1efe2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #15 0x7bf1efe2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #16 0x5d9675fcc844 in _start (/home/pop/sec/openexr/build-asan/bin/exrcheck+0xe844) (BuildId: 087c972343a5372940c42c0a2e7bce4a84288aec)

0x7bedc3934700 is located 256 bytes before 8590720784-byte region [0x7bedc3934800,0x7befc39f4710)
allocated by thread T0 here:
    #0 0x7bf1f40fd9c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x7bf1f115883e in internal_decode_alloc_buffer /home/pop/sec/openexr/src/lib/OpenEXRCore/coding.c:256
    #2 0x7bf1f100da97 in internal_exr_undo_piz /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:643
    #3 0x7bf1f115b038 in exr_uncompress_chunk /home/pop/sec/openexr/src/lib/OpenEXRCore/compression.c:546
    #4 0x7bf1f1161168 in exr_decoding_run /home/pop/sec/openexr/src/lib/OpenEXRCore/decoding.c:580
    #5 0x7bf1f2a71add in run_decode /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:586
    #6 0x7bf1f2a83dc4 in Imf_4_0::ScanLineInputFile::Data::readPixels(Imf_4_0::FrameBuffer const&, int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfScanLineInputFile.cpp:500
    #7 0x7bf1f28c6a81 in Imf_4_0::InputFile::Data::readPixels(int, int) /home/pop/sec/openexr/src/lib/OpenEXR/ImfInputFile.cpp:458
    #8 0x7bf1f3bfe2dc in readScanline<Imf_4_0::InputPart> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:239
    #9 0x7bf1f3c05b04 in readMultiPart /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:905
    #10 0x7bf1f3c126fd in runChecks<char const*> /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1171
    #11 0x7bf1f3c146b9 in Imf_4_0::checkOpenEXRFile(char const*, bool, bool, bool) /home/pop/sec/openexr/src/lib/OpenEXRUtil/ImfCheckFile.cpp:1835
    #12 0x5d9675fce8f8 in exrCheck(char const*, bool, bool, bool, bool) /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:96
    #13 0x5d9675fcb2b1 in main /home/pop/sec/openexr/src/bin/exrcheck/main.cpp:164
    #14 0x7bf1efe2a1c9 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #15 0x7bf1efe2a28a in __libc_start_main_impl ../csu/libc-start.c:360
    #16 0x5d9675fcc844 in _start (/home/pop/sec/openexr/build-asan/bin/exrcheck+0xe844) (BuildId: 087c972343a5372940c42c0a2e7bce4a84288aec)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pop/sec/openexr/src/lib/OpenEXRCore/internal_piz.c:148 in wdec14_4
Shadow bytes around the buggy address:
  0x7bedc3934480: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934500: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934580: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934680: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x7bedc3934700:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934780: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7bedc3934800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7bedc3934880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7bedc3934900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7bedc3934980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1711239==ABORTING

To prove this is both READ and WRITE, we can also memcheck against non-ASAN release build:

valgrind --tool=memcheck --leak-check=no --track-origins=no \
  --error-limit=no --num-callers=20 \
  ./build-relwithdebinfo/bin/exrcheck /tmp/piz_scanline_redzone.exr

Observed result:

This confirms the bug is an OOB read/write, not only a read-first crash.

Redzone-Oriented File

  • width: 67108862
  • height: 32
  • channel A: FLOAT, sampling 1 x 1
  • channel B: HALF, sampling 33554431 x 16

This makes:

width * 32 * 2 = 4294967168

which wraps signed 32-bit arithmetic to -128.

That places the next wavbuf access just before the allocated buffer, producing a clean heap-overflow report.

Impact

A crafted EXR file can trigger out-of-bounds memory access during PIZ decompression. The primitive includes both invalid reads and invalid writes. Depending on allocator layout and surrounding memory, this could lead to process crash, memory corruption, or potentially stronger exploitation outcomes.

Recommended Fix

  • compute channel span in 64-bit arithmetic
  • reject any overflow in nx * ny * wcount
  • validate cumulative per-channel decoded footprint against outsz before wavelet decode
  • fail decompression if channel-derived layout does not exactly fit the decompression buffer

Found by: Quang Luong of Calif.io

Affected Packages

3 total 3 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIopenexr3.1.0&&< 3.2.73.2.7pip install --upgrade 'openexr==3.2.7'
🐍PyPIopenexr3.3.0&&< 3.3.93.3.9pip install --upgrade 'openexr==3.3.9'
🐍PyPIopenexr3.4.0&&< 3.4.93.4.9pip install --upgrade 'openexr==3.4.9'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for openexr, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update openexr to 3.2.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-588r-cr5c-w6hf is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-588r-cr5c-w6hf can be triaged on real exposure rather than presence alone.

Tailored to GHSA-588r-cr5c-w6hf. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant
ProductFixed inAdvisory
Red Hat Enterprise Linux 10openexr-0:3.1.10-8.el10_1.2RHSA-2026:15888
Red Hat Enterprise Linux 10openexr-0:3.1.10-8.el10_2.2RHSA-2026:19146
Red Hat Enterprise Linux 10.0 Extended Update Supportopenexr-0:3.1.10-8.el10_0.2RHSA-2026:17656
Red Hat Enterprise Linux 9openexr-0:3.1.1-3.el9_7.2RHSA-2026:15887
Red Hat Enterprise Linux 9openexr-0:3.1.1-3.el9_8.2RHSA-2026:19359
Red Hat Enterprise Linux 9.0 Update Services for SAP Solutionsopenexr-0:3.1.1-2.el9_0.3RHSA-2026:19587
Red Hat Enterprise Linux 9.2 Update Services for SAP Solutionsopenexr-0:3.1.1-2.el9_2.3RHSA-2026:17659
Red Hat Enterprise Linux 9.4 Extended Update Supportopenexr-0:3.1.1-2.el9_4.3RHSA-2026:17658

Frequently Asked Questions

## Summary `internal_exr_undo_piz()` advances the working wavelet pointer with signed 32-bit arithmetic: ```c wavbuf += nx * ny * wcount; ``` Because `nx`, `ny`, and `wcount` are `int`, a crafted EXR file can make this product overflow and wrap. The next channel then decodes from an incorrect address. The wavelet decode path operates in place, so this yields both out-of-bounds reads and out-of-bounds writes. Tested on commit 7820b7e1b93405ba1d551c43a945018226b75bc5 ## Technical Details The vulnerable decode path is: 1. `internal_exr_undo_piz()` sets `wavbuf = decode->scratch_buffer_1`.
O3 Security · Impact-Aware SCA

Is GHSA-588r-cr5c-w6hf in your dependencies?

O3 Security finds GHSA-588r-cr5c-w6hf across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

GHSA-588r-cr5c-w6hf: openexr (High 7.8) | O3 Security