Segurium Research WP Go Maps
WP Go Maps 10.1.09 fixes an unauthenticated REST API denial of service
- Plugin
- WP Go Maps
wp-google-maps - Affected versions
- 10.1.08 and earlier
- Fixed in
10.1.09- Class
- Other
- Severity
- medium Segurium Research assessment. No CVSS score published yet.
- CVE
- None assigned at the time of writing
- Installs
- 300,000 active
- Patch released
- Sources
WP Go Maps, formerly WP Google Maps, shipped 10.1.09 on 2026-09-01. The plugin is installed on about 300,000 sites. The vendor changelog names the bug: “Fixed issue where a crafted unauthenticated request to the compressed REST API parameter parser could trigger an unbounded loop, causing excessive CPU usage and log file growth (denial of service). Security issue, thanks to Asim Alshaya”.
The diff backs that up. Five files changed, 27 lines added and 4 removed. Two of the five are credits pages that add the reporter’s name. One is the plugin header with the version bump. The two that matter are includes/class.rest-api.php and includes/class.elias-fano.php.
Both changed files gain the same kind of code: a bounds check on a pointer that arrives in the request. In 10.1.08 that pointer set the end condition of a decode loop, and nothing compared it to the buffer it indexed into.
What the release fixes
RestAPI::parseCompressedParameters() read the midcbp request parameter, cast it to an integer, and handed it to EliasFano::decode(). That value is the stop condition of the decoder’s main for loop. A value larger than the byte buffer made the loop read past the end of the array on every iteration until PHP stopped the request.
- Affected versions: 10.1.08 and earlier
- Fixed in: 10.1.09
- Class: denial of service through an unvalidated loop bound (
other) - Installs: ~300,000 active installs, per the wordpress.org plugin directory
How the bug works
The plugin accepts marker IDs in a compressed form so that a long list of IDs fits in a short parameter. RestAPI::parseCompressedParameters() in includes/class.rest-api.php unpacks that payload. Here is the code as it stood in 10.1.08, around line 201:
$compressed = array_values( unpack('C' . strlen($compressed), $compressed) );
$pointer = (int)$request['midcbp'];
$eliasFano = new EliasFano();
$markerIDs = $eliasFano->decode($compressed, (int)$request['midcbp']);
// TODO: Legacy markerIDs was a string, because this was historically more compact than POSTing an array. This can be altered, but the marker listing modules will have to be adjusted to cater for that
$request['markerIDs'] = implode(',', $markerIDs);
Follow midcbp from that first read. unpack('C' . strlen($compressed), $compressed) turns the payload into an array of single bytes. array_values() reindexes it from 0. So $compressed is a plain list, and its highest valid index is count($compressed) - 1.
$pointer = (int)$request['midcbp']; is the only handling the value gets. A PHP cast is not a check. A word becomes 0. A negative number stays negative. A very large number stays very large. The cast never fails and never rejects anything.
The old line then passed (int)$request['midcbp'] to decode() a second time, not the $pointer variable it had just built. That detail matters for the fix, and it is covered below.
The second argument of decode() is $compressedBufferPointer. In includes/class.elias-fano.php, that argument is the end of the loop:
$lowBitsCount = 0;
$lowBits = 0;
$cb = 1;
for(
$highBitsPointer = floor($lowBitsLength * $listCount / 8 + 6);
$highBitsPointer < $compressedBufferPointer;
Read the two halves of that loop separately. The start value, floor($lowBitsLength * $listCount / 8 + 6), comes from the data. The stop value comes from the request. The buffer being indexed is $compressedBuffer. Nothing ties the stop value to the size of that buffer.
So the request decides how far the loop walks, and the array decides nothing. Once $highBitsPointer passes the last real index, every read of $compressedBuffer[$highBitsPointer] finds no key. PHP 8 raises an “Undefined array key” warning for each of those reads. PHP 7 raises “Undefined offset”. When log_errors is on, each warning is one more write to the error log. The loop does not break on the missing key. It keeps counting up.
The loop ends in one of two ways. PHP hits max_execution_time and kills the request. Or the loop reaches the pointer value the attacker sent. Either way one worker process is busy the whole time, and the error log grows once per iteration.
That is the whole bug: an attacker-controlled integer used as a loop bound over an array it was never compared against.
10.1.09 fixes it in the REST layer first, before any decoding starts:
$pointer = (int)$request['midcbp'];
-
+
+ /* Security: midcbp is a fully attacker-controlled, unauthenticated value that
+ * drives EliasFano::decode()'s loop bound directly. A legitimate pointer can
+ * never exceed the buffer it indexes into, so anything outside that range is a
+ * malformed/malicious request - reject it here, before the expensive decode
+ * loop ever starts, rather than letting it run until max_execution_time while
+ * flooding the error log with out-of-bounds array warnings. */
+ if($pointer < 0 || $pointer > count($compressed))
+ throw new \Exception('Invalid compressed buffer pointer supplied for marker IDs');
+
$eliasFano = new EliasFano();
- $markerIDs = $eliasFano->decode($compressed, (int)$request['midcbp']);
+ $markerIDs = $eliasFano->decode($compressed, $pointer);
Two things changed on those lines. The new if rejects any pointer below 0 or above count($compressed). The call to decode() now passes $pointer instead of casting the raw request value again. Without that second change the check would guard a variable that the call never used.
The decoder gained its own guard as well:
+ /* Security: defence-in-depth against a caller passing an out-of-range pointer -
+ * this loop bound must never exceed the buffer it indexes into, or every
+ * iteration reads past the end of $compressedBuffer. The REST API layer already
+ * rejects this case outright (see RestAPI::parseCompressedParameters()), but this
+ * class is reusable, so it shouldn't rely solely on callers validating first. */
+ $compressedBufferLength = count($compressedBuffer);
+ if($compressedBufferPointer < 0 || $compressedBufferPointer > $compressedBufferLength)
+ $compressedBufferPointer = $compressedBufferLength;
The two guards behave differently on purpose. The REST layer throws and stops the request. EliasFano::decode() clamps the pointer to the buffer length and carries on, because other callers may pass a value that is merely wrong rather than hostile.
What an attacker gets is narrow. No file is read. No file is written. No code runs. No account changes. The gain is one busy PHP worker per request, plus error log growth on disk. Send enough of those requests and the site runs out of workers or the disk fills.
Two limits on the detail above. The diff shows the loop header and the variables above it, so the work done inside each iteration is not visible in these bytes. The diff also does not include the route registration or the permission callback for this endpoint. The claim that the request needs no login comes from the vendor changelog and from the vendor’s own comment in the patch, which calls midcbp “a fully attacker-controlled, unauthenticated value”. The exact REST route is not established by this diff.
Who is exposed
Sites running WP Go Maps 10.1.08 or earlier with the plugin active. The vendor states that no login is needed, so any visitor can reach the parser.
No setting has to be on. Nothing in the diff points at an option, a capability or a second plugin as a precondition. The code path runs whenever a request carries the compressed marker ID parameters.
Two server settings change how bad the result is. Error logging controls the disk half: with log_errors off, PHP writes no warning per iteration, and only the CPU cost stays. max_execution_time controls the CPU half: a low value ends each abusive request sooner, a high value lets it run longer.
Site data is not at risk from this bug. The pointer only moves an index inside a byte array that the same request supplied. It does not select a file, a table row or a user.
What to do
Update WP Go Maps to 10.1.09. That is the whole fix.
On a site that may have been hit, check the PHP error log for long runs of “Undefined array key” or “Undefined offset” warnings pointing at includes/class.elias-fano.php. Those warnings are the signature of the loop reading past the end of $compressedBuffer.
Check the size of the PHP error log and the free space on the disk. Log growth is the part of this bug that survives after the requests stop. Rotate or truncate the log if it grew large.
Check the web server access log for repeated requests to the plugin’s REST endpoints carrying a midcbp parameter, especially from one source in a short window. Pair that with PHP-FPM or process logs showing workers hitting max_execution_time.
The patch adds two bounds checks. RestAPI::parseCompressedParameters() now throws an exception when midcbp is negative or larger than the unpacked buffer, before EliasFano::decode() is ever called. EliasFano::decode() clamps the same value to the buffer length for any other caller. After updating to 10.1.09, the request that caused the loop is refused at the REST layer.