MapRoot: A Tale of Two Zero-Days, Two Patches, Two Bypasses Leading to Cross-Tenant RCE on Microsoft Planetary Computer | Enclave<br>Skip to main content<br>☰Scan free
Back to BlogMapRoot: A Tale of Two Zero-Days, Two Patches, Two Bypasses Leading to Cross-Tenant RCE on Microsoft Planetary Computer<br>Two zero-days in numexpr and GDAL gave us code execution inside Microsoft Planetary Computer. The real impact was RBAC: a popped pod could access cross-tenant secrets. Microsoft downgraded it, then quietly removed the permissions, and later reversed it back to critical.<br>Yanir TsarimiCo-founder & CPOMay 28, 2026
MSRC said there was no cross-tenant impact. Their RBAC diff said otherwise.<br>Microsoft runs a cloud service called Planetary Computer. We found two zero-day vulnerabilities in critical open-source libraries the service depends on. The bugs gave us code execution on Microsoft's servers, inside the service's internal network.<br>From there, the compromised process had permission to access every other customer's data on the platform. We documented that access path. We did not exercise it. Under coordinated disclosure rules, we are not supposed to.<br>We reported the bugs to Microsoft in February-March. The first was confirmed critical. Two weeks later, both cases were downgraded to "low severity, no impact on other customers."<br>So we went back to the live service and re-ran the same permission check we'd captured in our reports. The cluster-scoped access we had documented was gone. Microsoft had surgically removed every permission in the days after our reports, while triage was telling us those permissions never mattered.<br>If there was no risk, there was nothing to remove.<br>We bypassed both patches in under a day. Three weeks later, Microsoft reversed itself and confirmed cross-customer exposure had been real all along. Both bugs are now critical, fixed, and pending public CVE assignment.<br>The Supply-Chain Problem<br>This is not a story about Microsoft. It's a story about two foundational open-source libraries that process untrusted input by default.<br>numexpr has 99,187 GitHub dependents and 15.6 million PyPI downloads per month. It ships in Pandas, PyTables, and virtually every scientific-Python stack. GDAL has 457,000 monthly downloads and is the de-facto standard for raster processing—QGIS, PostGIS, GeoPandas, and the entire OSGeo ecosystem sit on top of it.<br>Microsoft's Planetary Computer exposed both libraries directly to user input. That configuration turned a general-purpose vulnerability into a cloud-scale compromise.<br>But the problem runs deeper. Any service that passes untrusted data to numexpr.evaluate() or GDAL's raster processing is one request away from code execution. We scanned for TiTiler deployments (an open-source tile server using numexpr) and found live instances at:<br>A major US public research university
Multiple private-sector analytics companies
US government cloud environments
A national research agency in Europe
Every one was a one-request RCE away from the same outcome we got on Azure.<br>Microsoft's bugs are fixed. The patch was bypassed. The libraries remain exposed. The numexpr maintainers have a patch ready but haven't released it yet.<br>If you use Pandas, QGIS, PostGIS, rasterio, or any geospatial or scientific-computing pipeline—assume you are running these libraries. Check your input boundaries. Assume the first patch can be bypassed.<br>Bug #1: numexpr, the Sandbox That Wasn't<br>numexpr.evaluate("1+1") looks innocent. Underneath, it calls Python's eval() against a restricted-globals namespace. The sanitizer is a regex blocklist plus a namespace where unknown names get replaced with inert VariableNode placeholders: dunders are filtered, brackets and colons are filtered, attribute access is filtered.<br>A 2023 CVE (CVE-2023-39631) against LangChain's calculator chain had already shown the blocklist was leaky. The numexpr maintainers patched that specific bypass. The sandbox model itself was untouched.<br>We found a new bypass that turned the entire sandbox into a one-liner. Three pieces:<br>Generator expression scope. A (... for x in ...) body has its own code object with its own co_names. numexpr's VariableNode shadowing applies to the outer code object's names, not the inner one. Inside a generator, eval resolves to the real __builtins__['eval'], not the placeholder.
Star-unpacking forces iteration. (*(genexpr),) is a syntactic construct that causes Python to drain the generator during eval(), meaning the generator body executes as a side effect, before numexpr ever inspects the return value.
Single-quoted strings bypass the regex. numexpr's sanitizer strips single-quoted strings before running the blocklist regex. Anything dangerous hidden inside '...' is invisible to the filter.
Chain them:<br>1+1+(*(eval(x) for x in ('__import__("os").popen("COMMAND").read()',)),)1+1 is the math part. numexpr starts evaluating it as normal arithmetic. The generator fires inside eval(). The payload is hidden in single quotes,...