# Root Cause Analysis — CVE-2026-33721

## Summary

MapServer versions 4.2.0 through 8.6.0 contain a heap-buffer-overflow vulnerability in the OGC SLD (Styled Layer Descriptor) XML parser. The function `msSLDParseRasterSymbolizer` in `src/mapogcsld.cpp` allocates a fixed-size buffer for 100 threshold pointers when parsing a `<se:Categorize>` element. The reallocation guard incorrectly checks `nValues == nMaxThreshold` instead of `nThresholds == nMaxThreshold`. Because `nValues` and `nThresholds` increment at different rates, the buffer is never expanded when more than 100 `<se:Threshold>` children are present, causing subsequent pointer writes to spill past the 800-byte (100 × 8) array boundary. The bug is reachable unauthenticated via the `SLD_BODY` parameter in a WMS `GetMap` request.

## Impact

- **Package**: OSGeo MapServer (`mapserv` CGI binary)
- **Affected versions**: 4.2.0 — 8.6.0 (last vulnerable tag: `rel-8-6-0`)
- **Fixed version**: 8.6.1 (`rel-8-6-1`)
- **Risk level**: High (CVSS 3.1: 7.5)
- **Consequences**: At minimum, an attacker can crash the MapServer worker process (denial of service). Depending on heap layout and input control, the overflowed pointers may be further exploitable.

## Root Cause

In `src/mapogcsld.cpp`, around line 2880, `msSLDParseRasterSymbolizer` initializes:

```cpp
int nMaxThreshold = 100;
char **papszThresholds = (char **)msSmallMalloc(sizeof(char *) * nMaxThreshold);
```

As the parser iterates over `<se:Categorize>` children, every `<se:Threshold>` increments `nThresholds` and stores a pointer into `papszThresholds`. The growth guard is meant to reallocate the array when it fills:

```cpp
// VULNERABLE (rel-8-6-0):
if (nValues == nMaxThreshold) {
    nMaxThreshold += 100;
    papszThresholds = (char **)msSmallRealloc(
        papszThresholds, sizeof(char *) * nMaxThreshold);
}
```

However, `nValues` tracks a *different* counter (the number of `<se:Value>` elements), while `nThresholds` tracks the actual number of threshold entries. When an SLD contains more than 100 thresholds, `nThresholds` exceeds 100 but `nValues` may still be much lower (e.g., 1), so reallocation never occurs. The next `papszThresholds[nThresholds] = …` write lands beyond the 100-slot buffer, producing a heap-buffer-overflow.

**Fix commit**: `ddd246b90acc6c7f920dfd056f33613cebe9154d`  
**Merge commit**: `7dbe91b`  
The patch changes exactly one line:

```diff
-          if (nValues == nMaxThreshold) {
+          if (nThresholds == nMaxThreshold) {
```

This ensures the array is resized based on the same counter that tracks live entries.

## Reproduction Steps

The complete reproduction is automated in `repro/reproduction_steps.sh`. It performs the following:

1. Clones MapServer at `rel-8-6-0` (vulnerable) and `rel-8-6-1` (fixed).
2. Builds both `mapserv` binaries with AddressSanitizer (`-fsanitize=address`).
3. Generates a minimal `test.map`, a tiny GeoTIFF (`tiny.tif`), and a malicious SLD payload (`payload.sld`) containing 200 `<se:Threshold>` elements.
4. Invokes the vulnerable binary through the real CGI path (`QUERY_STRING` + `REQUEST_METHOD=GET`) with the SLD payload via `SLD_BODY`.
5. Captures the ASAN crash log (`logs/vulnerable_asan.txt`).
6. Runs the same request against the fixed binary and verifies it returns a valid PNG image with no ASAN error (`logs/fixed_response.txt`).

## Evidence

- **Vulnerable ASAN log**: `logs/vulnerable_asan.txt`
  - Key excerpt:
    ```
    ==10268==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x518000008fa0
    WRITE of size 8 at 0x518000008fa0 thread T0
        #0 0x7fd97611c7d4 in msSLDParseRasterSymbolizer /.../src/mapogcsld.cpp:2895
    0x518000008fa0 is located 0 bytes after 800-byte region [0x518000008c80,0x518000008fa0)
    ```
- **Fixed response**: `logs/fixed_response.txt`
  - Starts with `Content-Type: image/png` and contains a valid PNG stream, confirming the fixed binary processes the same payload without crashing.
- **Runtime manifest**: `repro/runtime_manifest.json` — records binary paths, tags, crash signature, and payload details.
- **Validation verdict**: `repro/validation_verdict.json` — marks status `confirmed`.

## Recommendations / Next Steps

1. **Immediate fix**: Upgrade to MapServer 8.6.1 or later. The patch is a single-line bound-check correction and carries no functional regression.
2. **Defensive measure**: If upgrading is not immediately possible, restrict the `SLD_BODY` / `SLD` query parameters at the reverse-proxy or WAF level, or disable WMS SLD support in the MapServer configuration.
3. **Testing**: Add a regression test that submits an SLD with >100 thresholds and asserts the process does not crash.
4. **Code-review note**: When arrays are grown dynamically, always use the same counter for both indexing and resize checks.

## Additional Notes

- **Idempotency**: `repro/reproduction_steps.sh` was run twice consecutively with identical results (confirmed heap-buffer-overflow on vulnerable, valid PNG on fixed).
- **Edge cases**: The crash is triggered with any SLD containing >100 `<se:Threshold>` children inside a `<se:Categorize>` block. The exact threshold count is 100; 101 is sufficient to overflow. The reproduction uses 200 to provide a clear safety margin.
- **Limitations**: The reproduction requires a functional GDAL/PROJ/libxml2 environment. The script installs missing Debian/Ubuntu packages automatically if they are absent.
