Skip to content

CVE-2026-33721: Verified Repro With Script Download

CVE-2026-33721: MapServer: heap-buffer-overflow in SLD Categorize parser msSLDParseRasterSymbolizer

CVE-2026-33721 is verified against mapserver · c. Affected versions: 4.2.0 - 8.6.0. Fixed in 8.6.1. This medium reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00183.

REPRO-2026-00183 mapserver · c Variant found May 28, 2026 CVE entry ↗ .txt
Severity
MEDIUM
CVSS
5.3
Reproduced in
14m 33s
Tool calls
177
Spend
$1.33
01 · Overview

What Is CVE-2026-33721?

CVE-2026-33721 is a high-severity heap-buffer-overflow (out-of-bounds write, CWE-787) in MapServer's OGC SLD parser. Parsing a crafted <se:Categorize> element can overflow a fixed-size buffer. Pruva reproduced it (reproduction REPRO-2026-00183).

02 · Severity & CVSS

CVE-2026-33721 Severity & CVSS Score

CVE-2026-33721 is rated medium severity, with a CVSS base score of 5.3 out of 10.

MEDIUM threat level
5.3 / 10 CVSS base
Weakness CWE-787 (Out-of-bounds Write) — Out-of-bounds Write

Medium — meaningful risk under specific conditions. Schedule a fix in the normal cycle.

03 · Affected Versions

Affected mapserver Versions

mapserver · c versions 4.2.0 - 8.6.0 are affected.

How to Reproduce CVE-2026-33721

$ pruva-verify REPRO-2026-00183
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00183/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-33721

Reproduced by Pruva's autonomous agents — 177 tool calls over 15 min. Full root-cause analysis and the complete transcript are below.

Variants tested

Systematic variant analysis of CVE-2026-33721 found no bypass or alternate trigger. The single-line fix (nValues->nThresholds in mapogcsld.cpp:2897) is logically complete. Five distinct payloads/entry points were tested on both vulnerable (rel-8-6-0) and fixed (rel-8-6-1) builds; none bypassed the fix.

How the agent worked 608 events · 177 tool calls · 15 min
15 minDuration
177Tool calls
141Reasoning steps
608Events
3Dead-ends
Agent activity over 15 min
Support
15
Repro
308
Variant
281
0:0014:33

Root Cause and Exploit Chain for CVE-2026-33721

Versions: 4.2.0 — 8.6.0 (last vulnerable tag: rel-8-6-0)Fixed: 8.6.1 (rel-8-6-1)

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.

  • 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:

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:

// 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:

-          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.

Variant Analysis & Alternative Triggers for CVE-2026-33721

Versions: 4.2.0 — 8.6.0 (rel-8-6-0)Fixed: 8.6.1 (rel-8-6-1)

CVE-2026-33721 is a heap-buffer-overflow in MapServer's SLD XML parser caused by a single-variable typo in the reallocation guard of msSLDParseRasterSymbolizer. The fix (rel-8-6-1) changes nValues == nMaxThreshold to nThresholds == nMaxThreshold. After exhaustive variant testing and code audit, no bypass or alternate trigger was found. The fix is logically complete: the increment of nThresholds and the reallocation guard now use the same variable, making the overflow path impossible under all tested input shapes. Different WMS operations (GetLegendGraphic, GetStyles) reach the same vulnerable sink through identical intermediate parsing functions, so they are not distinct variants—they share the same surface.

Fix Coverage / Assumptions

The original fix assumes:

  1. The only defect was the wrong counter in the papszThresholds reallocation condition.
  2. nThresholds is the authoritative count of threshold elements that have been written into papszThresholds.
  3. nValues == nMaxValues (the adjacent guard for papszValues) is already correct.

The fix covers:

  • All SLD_BODY inline payloads delivered through any WMS operation that calls msSLDApplySLD.
  • All SLD URL-reference payloads delivered through any WMS operation that calls msSLDApplySLDURL.

The fix does not cover:

  • A hypothetical integer-overflow of nThresholds / nMaxThreshold (requires billions of elements, not practical via HTTP).
  • Memory-exhaustion DoS from extremely large SLD documents (different class, no hard cap added).
  • Any unrelated bugs in other SLD symbolizer parsers.

Variant / Alternate Trigger Attempts

We tested 5 distinct variants against both the vulnerable (rel-8-6-0) and fixed (rel-8-6-1) builds:

Variant 1 — Different WMS operation: GetLegendGraphic
  • Entry point: REQUEST=GetLegendGraphic&SLD_BODY=...
  • Vulnerable result: Heap-buffer-overflow (ASAN crash at mapogcsld.cpp:2895).
  • Fixed result: No crash; returns a valid PNG legend image.
  • Assessment: Same sink reached through the same msSLDApplySLDmsSLDParseSLDmsSLDParseRasterSymbolizer path. Not a distinct variant.
Variant 2 — Different WMS operation: GetStyles
  • Entry point: REQUEST=GetStyles&SLD_BODY=...
  • Vulnerable result: Heap-buffer-overflow (ASAN crash at mapogcsld.cpp:2895).
  • Fixed result: No crash; returns a valid SLD response.
  • Assessment: Same sink and same intermediate path as the original. Not a distinct variant.
Variant 3 — Non-standard XML ordering (values-heavy)
  • Entry point: REQUEST=GetMap&SLD_BODY= with 200 <se:Value> elements and only 1 <se:Threshold>.
  • Vulnerable result: No crash. The papszValues guard is correct (nValues == nMaxValues), so the values array reallocates safely. The thresholds array (size 100) is never exceeded.
  • Fixed result: No crash.
  • Assessment: This configuration does not trigger the bug on the vulnerable version. No new variant surface.
Variant 4 — Only Thresholds, no initial Value
  • Entry point: REQUEST=GetMap&SLD_BODY= with 200 <se:Threshold> elements and zero <se:Value> elements after the LookupValue.
  • Vulnerable result: No crash. The post-processing loop requires nValues == nThresholds + 1, which fails (0 != 201), so the code skips the array access loop. However, note that the loop in msSLDParseRasterSymbolizer that populates the arrays still would write past the bounds during parsing if it continued—but the ASAN run did not show a crash. This is because the write at papszThresholds[nThresholds] happens during the while loop, and with 200 thresholds and 1 initial LookupValue (which is not a Value), nValues stays at 0. The check nValues == nMaxThreshold is 0 == 100 = FALSE, so the array should overflow. Yet no crash was observed. Re-examining the code shows that CPLGetXMLNode(psCategorize, "Value") returns the first Value child; if there is none, the while loop body is skipped entirely. Thus, with zero Value elements, the loop never executes and no thresholds are processed. This is a useful finding for understanding the parser's edge-case behavior, but it is not a bypass.
  • Fixed result: No crash.
  • Assessment: No new variant; the loop simply does not execute without an initial Value node.
Variant 5 — Large threshold count (integer-overflow probe)
  • Entry point: REQUEST=GetMap&SLD_BODY= with 500 <se:Threshold> elements.

  • Vulnerable result: Heap-buffer-overflow (ASAN crash at mapogcsld.cpp:2895).

  • Fixed result: No crash; reallocation proceeds correctly (100→200→300→400→500).

  • Assessment: Confirms the fix scales correctly. No bypass.

  • Package: OSGeo MapServer (mapserv CGI binary)

  • Affected versions: 4.2.0 — 8.6.0 (rel-8-6-0)

  • Fixed version: 8.6.1 (rel-8-6-1)

  • Risk level: High (CVSS 3.1: 7.5)

  • Consequences: Worker-process crash (DoS) via unauthenticated WMS request. The overflow is a pointer-array write, so stronger exploitation depends on heap layout and is not guaranteed.

Root Cause

The underlying bug is a copy-paste typo in a dynamic-array reallocation guard:

} else if (strcasecmp(psNode->pszValue, "Threshold") == 0) {
  papszThresholds[nThresholds] = psNode->psChild->pszValue;
  nThresholds++;
  if (nValues == nMaxThreshold) {   // BUG: should be nThresholds
    nMaxThreshold += 100;
    papszThresholds = (char **)msSmallRealloc(
        papszThresholds, sizeof(char *) * nMaxThreshold);
  }
}

Because nValues and nThresholds increment at different rates (one per Value element vs. one per Threshold element), the guard can remain false even after nThresholds exceeds the allocated array size. The fix commit fb08dad4 corrects the guard to nThresholds == nMaxThreshold, which is logically airtight.

Reproduction Steps

  1. See vuln_variant/reproduction_steps.sh for the automated variant matrix.
  2. The script:
    • Re-runs the original GetMap + SLD_BODY baseline on both vulnerable and fixed builds.
    • Re-runs the same payload through GetLegendGraphic and GetStyles.
    • Tests non-standard XML orderings (values-heavy, thresholds-only).
    • Tests a 500-threshold payload as an integer-overflow / large-scale probe.
    • Logs ASAN output for every run under logs/variant_*_vuln.log and logs/variant_*_fixed.log.
  3. Expected evidence:
    • Vulnerable build crashes on all threshold-heavy payloads (ASAN heap-buffer-overflow).
    • Fixed build returns valid WMS output or an OGC error XML/PNG without ASAN errors.
    • No bypass payload causes a crash on the fixed build.

Evidence

  • Baseline crash (vulnerable): logs/variant_baseline_vuln.log
  • Baseline clean (fixed): logs/variant_baseline_fixed.log
  • GetLegendGraphic crash (vulnerable): logs/variant_legendgraphic_vuln.log
  • GetLegendGraphic clean (fixed): logs/variant_legendgraphic_fixed.log
  • GetStyles crash (vulnerable): logs/variant_getstyles_vuln.log
  • GetStyles clean (fixed): logs/variant_getstyles_fixed.log
  • Values-heavy attempt: logs/variant_reordered_vuln.log, logs/variant_reordered_fixed.log
  • Thresholds-only attempt: logs/variant_onlythresholds_vuln.log, logs/variant_onlythresholds_fixed.log
  • Large-count attempt: logs/variant_overflowprobe_vuln.log, logs/variant_overflowprobe_fixed.log

All logs are stored under logs/ in the run directory.

Recommendations / Next Steps

  1. The fix is complete. No additional patch is required for this specific CVE.
  2. Regression test: The MapServer project should add an automated test that submits an SLD with >100 <se:Threshold> elements and asserts the process does not crash. Such a test would have caught this typo immediately.
  3. Code audit suggestion: While no similar typos were found in the current codebase, a one-time audit of all msSmallMalloc/msSmallRealloc pairs in src/mapogcsld.cpp and src/mapogcfilter.cpp is a low-cost defensive measure.
  4. Hardening: Consider adding an upper bound on the number of Categorize children (e.g., 10,000) to prevent memory-exhaustion DoS from pathological SLD documents.

Additional Notes

  • Idempotency: The vuln_variant/reproduction_steps.sh script was run twice and produced identical results both times.
  • Environment: Both vulnerable and fixed mapserv binaries were built with AddressSanitizer (-fsanitize=address) in Debug mode. The tests used the existing repro/test.map, repro/tiny.tif, and repro/mapserver.conf from the reproduction stage.
  • No trust-boundary issue: All tested entry points are standard, unauthenticated WMS operations. The SLD_BODY / SLD parameters are explicitly designed to accept untrusted XML from remote clients.

CVE-2026-33721 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:000:43
0:00
session startedaccounts/fireworks/models/kimi-k2p6 · cve-2026-33721 · cve-2026
0:22
0:22
0:22
error

Unknown error

0:22
web search
0:22
error

Unknown error

0:36
0:36
extract_facts
no facts extracted
0:37
0:37
0:37
supportrepro
0:39
0:40
0:40
0:40
0:40
0:40
0:40
0:41
0:41
0:42
0:42
0:43

Artifacts and Evidence for CVE-2026-33721

Scripts, logs, diffs, and output captured during the reproduction.

08 · How to Fix

How to Fix CVE-2026-33721

Upgrade mapserver · c to 8.6.1 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-33721 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-33721

How is CVE-2026-33721 reached?

MapServer is a CGI binary that parses OGC SLD XML to style layers served over WMS/WFS. A request supplying an SLD with a <se:Categorize> element containing more than 100 thresholds drives the off-by-one reallocation guard and overflows the heap buffer.

Which MapServer versions are affected by CVE-2026-33721, and where is it fixed?

MapServer versions 4.2.0 through 8.6.0 are affected. It is fixed in 8.6.1 — upgrade to 8.6.1 or later.

How can I reproduce CVE-2026-33721?

Download the verified script from this page and run it in an isolated environment against MapServer 4.2.0-8.6.0. It submits an SLD with an oversized <se:Categorize> threshold list and shows the heap-buffer-overflow that 8.6.1 fixes.
11 · References

References for CVE-2026-33721

Authoritative sources for CVE-2026-33721 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.