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.
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).
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 — meaningful risk under specific conditions. Schedule a fix in the normal cycle.
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 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 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.
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
Root Cause and Exploit Chain for CVE-2026-33721
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 (
mapservCGI 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:
- Clones MapServer at
rel-8-6-0(vulnerable) andrel-8-6-1(fixed). - Builds both
mapservbinaries with AddressSanitizer (-fsanitize=address). - Generates a minimal
test.map, a tiny GeoTIFF (tiny.tif), and a malicious SLD payload (payload.sld) containing 200<se:Threshold>elements. - Invokes the vulnerable binary through the real CGI path (
QUERY_STRING+REQUEST_METHOD=GET) with the SLD payload viaSLD_BODY. - Captures the ASAN crash log (
logs/vulnerable_asan.txt). - 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)
- Key excerpt:
- Fixed response:
logs/fixed_response.txt- Starts with
Content-Type: image/pngand contains a valid PNG stream, confirming the fixed binary processes the same payload without crashing.
- Starts with
- Runtime manifest:
repro/runtime_manifest.json— records binary paths, tags, crash signature, and payload details. - Validation verdict:
repro/validation_verdict.json— marks statusconfirmed.
Recommendations / Next Steps
- Immediate fix: Upgrade to MapServer 8.6.1 or later. The patch is a single-line bound-check correction and carries no functional regression.
- Defensive measure: If upgrading is not immediately possible, restrict the
SLD_BODY/SLDquery parameters at the reverse-proxy or WAF level, or disable WMS SLD support in the MapServer configuration. - Testing: Add a regression test that submits an SLD with >100 thresholds and asserts the process does not crash.
- 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.shwas 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
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:
- The only defect was the wrong counter in the
papszThresholdsreallocation condition. nThresholdsis the authoritative count of threshold elements that have been written intopapszThresholds.nValues == nMaxValues(the adjacent guard forpapszValues) is already correct.
The fix covers:
- All
SLD_BODYinline payloads delivered through any WMS operation that callsmsSLDApplySLD. - All
SLDURL-reference payloads delivered through any WMS operation that callsmsSLDApplySLDURL.
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
msSLDApplySLD→msSLDParseSLD→msSLDParseRasterSymbolizerpath. 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
papszValuesguard 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 theLookupValue. - 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 inmsSLDParseRasterSymbolizerthat 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 atpapszThresholds[nThresholds]happens during thewhileloop, and with 200 thresholds and 1 initialLookupValue(which is not aValue),nValuesstays at 0. The checknValues == nMaxThresholdis0 == 100= FALSE, so the array should overflow. Yet no crash was observed. Re-examining the code shows thatCPLGetXMLNode(psCategorize, "Value")returns the firstValuechild; if there is none, the while loop body is skipped entirely. Thus, with zeroValueelements, 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
Valuenode.
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 (
mapservCGI 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
- See
vuln_variant/reproduction_steps.shfor the automated variant matrix. - The script:
- Re-runs the original
GetMap+SLD_BODYbaseline on both vulnerable and fixed builds. - Re-runs the same payload through
GetLegendGraphicandGetStyles. - 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.logandlogs/variant_*_fixed.log.
- Re-runs the original
- 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.
- Vulnerable build crashes on all threshold-heavy payloads (ASAN
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
- The fix is complete. No additional patch is required for this specific CVE.
- 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. - Code audit suggestion: While no similar typos were found in the current codebase, a one-time audit of all
msSmallMalloc/msSmallReallocpairs insrc/mapogcsld.cppandsrc/mapogcfilter.cppis a low-cost defensive measure. - Hardening: Consider adding an upper bound on the number of
Categorizechildren (e.g., 10,000) to prevent memory-exhaustion DoS from pathological SLD documents.
Additional Notes
- Idempotency: The
vuln_variant/reproduction_steps.shscript was run twice and produced identical results both times. - Environment: Both vulnerable and fixed
mapservbinaries were built with AddressSanitizer (-fsanitize=address) in Debug mode. The tests used the existingrepro/test.map,repro/tiny.tif, andrepro/mapserver.conffrom the reproduction stage. - No trust-boundary issue: All tested entry points are standard, unauthenticated WMS operations. The
SLD_BODY/SLDparameters 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.
Unknown error
Unknown error
Artifacts and Evidence for CVE-2026-33721
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-33721
Upgrade mapserver · c to 8.6.1 or later.
FAQ: CVE-2026-33721
How is CVE-2026-33721 reached?
Which MapServer versions are affected by CVE-2026-33721, and where is it fixed?
How can I reproduce CVE-2026-33721?
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.