GitHub Security Advisory: GHSA-24p2-j2jr-386w
Package: psd-tools (PIP)
Severity: MODERATE (CVSS 0.0)
CWE: CWE-190: Integer Overflow or Wraparound, CWE-409: Improper Handling of Highly Compressed Data (Data Amplification), CWE-617: Reachable Assertion, CWE-704: Incorrect Type Conversion or Cast, CWE-755: Improper Handling of Exceptional Conditions
Vulnerable versions: < 1.12.2
Patched in: 1.12.2

Summary: psd-tools: Compression module has unguarded zlib decompression, missing dimension validation, and hardening gaps

Details:
## Summary

A security review of the `psd_tools.compression` module (conducted against the `fix/invalid-rle-compression` branch, commits `7490ffa`–`2a006f5`) identified the following pre-existing issues. The two findings introduced and **fixed** by those commits (Cython buffer overflow, `IndexError` on lone repeat header) are excluded from this report.

---

## Findings

### 1. Unguarded `zlib.decompress` — ZIP bomb / memory exhaustion (Medium)

**Location**: `src/psd_tools/compression/__init__.py`, lines 159 and 162

```python
result = zlib.decompress(data)          # Compression.ZIP
decompressed = zlib.decompress(data)    # Compression.ZIP_WITH_PREDICTION
```

`zlib.decompress` is called without a `max_length` cap. A crafted PSD file containing a ZIP-compressed channel whose compressed payload expands to gigabytes would exhaust process memory before any limit is enforced. The RLE path is not vulnerable to this because the decoder pre-allocates exactly `row_size × height` bytes; the ZIP path has no equivalent ceiling.

**Impact**: Denial-of-service / OOM crash when processing untrusted PSD files.

**Suggested mitigation**: Pass a reasonable `max_length` to `zlib.decompress`, derived from the expected `width * height * depth // 8` byte count already computed in `decompress()`.

---

### 2. No upper-bound validation on image dimensions before allocation (Low)

**Location**: `src/psd_tools/compression/__init__.py`, lines 138 and 193

```python
length = width * height * max(1, depth // 8)   # decompress()
row_size = max(width * depth // 8, 1)           # decode_rle()
```

Neither `width`, `height`, nor `depth` are range-checked before these values drive memory allocation. The PSD format (version 2 / PSB) permits dimensions up to 300,000 × 300,000 pixels; a 4-channel 32-bit image at that size would require ~144 TB to hold. While the OS/Python allocator will reject such a request, there is no early, explicit guard that produces a clean, user-facing error.

**Impact**: Uncontrolled allocation attempt from a malformed or adversarially crafted PSB file; hard crash rather than a recoverable error.

**Suggested mitigation**: Validate `width`, `height`, and `depth` against known PSD/PSB limits before entering decompression, and raise a descriptive `ValueError` early.

---

### 3. `assert` used as a runtime integrity check (Low)

**Location**: `src/psd_tools/compression/__init__.py`, line 170

```python
assert len(result) == length, "len=%d, expected=%d" % (len(result), length)
```

This assertion can be silently disabled by running the interpreter with `-O` (or `-OO`), which strips all `assert` statements. If the assertion ever becomes relevant (e.g., after future refactoring), disabling it would allow a length mismatch to propagate silently into downstream image compositing.

**Impact**: Loss of an integrity guard in optimised deployments.

**Suggested mitigation**: Replace with an explicit `if` + `raise ValueError(...)`.

---

### 4. `cdef int` indices vs