Popping Microsoft’s Sandbox: Dataverse Security Risks in Plugin Containers
Jun 24, 2026
Dataverse security usually centers on access controls, roles, and environment governance, but this research examines what a custom .NET plugin could expose from inside a Microsoft Dataverse sandbox container.
Authors:
Simon Maxwell-Stewart
Staff Security Researcher
Phantom Labs™
BeyondTrust
Popping Microsoft’s Sandbox: Dataverse Security Risks in Plugin Containers
Simon Maxwell-Stewart
Staff Security Researcher
Phantom Labs™
BeyondTrust
What Falls Out of a Dataverse Container
Link copied
Microsoft Dataverse lets you deploy custom .NET plugins that run server-side in process-isolated Windows Server containers. We deployed one. Within minutes, we had SYSTEM privileges on the box, debug access to every running process, a production TLS private key for Microsoft's sandbox infrastructure, a live internal Microsoft API key verified against production endpoints, 51 other organization GUIDs from co-located tenants, and 69 proprietary Microsoft DLLs that were never meant to leave that container.
Background: How This Research Extends the Copilot Studio Security Series
Link copied
Sometimes learning about Azure feels more like an exercise in archaeology than engineering. This is certainly the case when trying to learn Microsoft’s no-code AI agent building platform: Copilot Studio.
In Part 1 Copilot Studio AI agents security risks, Phantom Labs™ covered Copilot Studio and its underlying architecture, we covered the basics of Copilot Studio and its underlying architecture. To recap: Copilot Studio (formerly Power Virtual Agents) is part of the Power Platform, which was built around Dataverse (formerly the Common Data Service), a direct descendant of the Dynamics CRM data platform (XRM).
Figure 1: The madness of Azure’s naming (renaming) conventions
The importance of this lineage is that Copilot Studio AI agents can inherit some pretty old, wacky features from the XRM. This blog focuses on one of them: Custom API Plugins.
Custom API Plugins
Link copied
Plugins were originally built for XRM to allow developers to register .NET assemblies against the platform’s event bus. This allowed for custom business logic in response to system messages triggered by events like record creation, updates, or deletion.
Custom API plugins extended this concept further, allowing developers to define their own custom messages. Once a Custom API Plugin has been registered, Dataverse exposes a full-blown Rest API endpoint to allow these custom messages to be triggered.
For example, a developer could define a new `ApproveExpenseReport` message and upload a .NET assembly to the Dataverse. After which, the Dataverse exposes the following:
which will send an `ApproveExpenseReport` message to the registered .NET assembly.
Adventures Registering Our Own Plugin
Link copied
Phantom Labs realised that we had many such .NET assemblies sitting in our out-the-box Dataverse environment and we were able to download and decompile the binaries to their source code using XRM Toolbox. After realising we could decompile these .NET plugins and inspect their source code, we became intrigued as to what kind of attack surface these plugins exposed. So we decided to register one.
Figure 2: Echo plugin that runs commands it receives as messages and returns the result
While this plugin allowed us to probe the environment, we quickly realized that more sophisticated attacks would be harder to execute. Since the plugin requests first hit a load balancer, meaning back-to-back calls were routed to different containers.
Instead, we used the Echo Plugin as a beachhead to setup a command and control (C2) channel via Azure Blob storage. Once active, the container polls an Azure bucket for commands and writes the results back to the C2 bucket, establishing persistent access for the lifetime of the container (~15-30 mins).
Figure 3: Our homespun C2
Getting SYSTEM on the Dataverse Container
Link copied
Now that we’ve landed on the box as the user “user manager\containeradministrator”, which belongs to the “BUILTIN\Administrators” group. Moreover, inspecting the user’s security profile revealed critical privileges of the user were enabled:
SeDebugPrivilege: Open ANY process on the box regardless of its owner. Read/write its memory (e.g., dump any process, inject into SandboxWorker.exe, etc.)
SeImpersonatePrivilege: Steal another user's token and act on their behalf. Enables "potato" style attacks.
SeCreateGlobalPrivilege: Create objects in the global namespace.
SeChangeNotifyPrivilege: Bypass file/directory traverse checking. Minor but ensures you can walk any path on the system.
Figure 4: Our C2 in action, listing the privileges of the user running our plugin code
As a member of the Administrators group, we could interact directly with the Service Control Manager (SCM). This allowed us to create custom services and choose which user it will run under.
This registers and starts a new service called SystemSvc as the LocalSystem aka “NT AUTHORITY\SYSTEM”. Because this isn’t a proper service implementation, it ultimately errors out—but not before running our chosen command as SYSTEM and writing the output to a file.
What Falls Out
Link copied
Now that we've escalated to SYSTEM on the box, let's do some fun things 😈.
SeDebugPrivilege is enabled by default, giving us debug access to every running process. Outbound HTTPS is completely unrestricted: no egress filtering, no proxy, no DLP. Anything we find, we can send straight to the internet.
Certificates and Private Keys
Next, we pulled the certificate store. The container held a TLS certificate and its corresponding RSA 2048-bit private key for `wus107.prd.sbx.dynamics.com`, a Microsoft-internal sandbox infrastructure hostname that doesn't resolve in public DNS. The private key is marked exportable and came out with a simple `certutil` call. This result was uploaded to our Azure Blob bucket over unrestricted outbound HTTPS.
Figure 5: A live TLS certificate we pulled from the box
We also found a self-signed worker certificate with a `Plex_Nonce` embedded in the Common Name. We'll come back to what that nonce is and why it matters shortly.
Process Memory
The Dataverse container runs a single critical process: `Microsoft.CDSRuntime.SandboxWorker.exe`, the gRPC worker responsible for executing plugins. With SeDebugPrivilege, we could read its entire address space.
We scanned 20 MB of the worker's memory and found 52 GUIDs that we can confirm are Dataverse organization IDs. Our own org ID appeared among them, positively identified via the sidecar's `GetWorkerAssignedMetadata` response (labeled as `CDS/OrganizationId:53c71faa-...`.
Out of the 52 GUIDs, 43 matched exactly with org IDs found in the sidecar's `AppInsightsConnectionString`, a telemetry routing table that maps every organization on the cluster to its instrumentation key. The remaining 9 GUIDs did not appear in the sidecar data, leaving their origins unknown. This cross-tenant information leak was confirmed with Microsoft on a call.
The Plex DLL Stack
We also walked away with 46 proprietary Microsoft DLLs comprising the entire Plex infrastructure stack. After decompiling them into roughly 14,000 C# source files using ILSpy, what began as a container escape turned into a full infrastructure audit.
The decompiled source revealed a host-side service called the "sidecar" that the worker connects to on startup, the methods it exposes, the parameters they accept, and how authentication is (or isn't) enforced.
Environment Variables
Next, we dumped the container's environment variables. Buried within the standard Windows noise, `sidecarIP` and `SideCarShimEndpointPort` gave us the address of the host-side gRPC sidecar service.
The `Plex_Nonce` is a 128-bit GUID that turned out to be the sole authentication credential required for the worker execution RPC. Meanwhile, `AzureTenantId` contained four Microsoft tenant IDs, including `microsoft.com` corporate (`72f988bf`) and `dynamics.com` (`f8cdef31`). Finally, the `CS_CLUSTER*` variables revealed the infrastructure topology: cluster `prdil107wus`, type `islandcluster`, and region `westus`.
Figure 6: A partial snapshot of the Environment variables revealing the sidecar configuration
With the decompiled source code, an IP address, a port number, and a stolen nonce, it was time to see what the sidecar had to say for itself.
The gRPC Architecture
Link copied
The decompiled source revealed a four-tier gRPC architecture that Microsoft calls "Plex." We reconstructed the full service definitions from the C# stubs and published them as an OpenAPI specification alongside Go tooling (link below). We also published a website called plex-explorer, documenting the plex architecture.
Figure 7: A snapshot from plex-explorer showing the sandbox architecture
Platform is the Dataverse application server tier. It dispatches plugin execution requests, processes all plugin callbacks (data queries, token acquisition), and makes every authorization decision. If a plugin wants to read data or get a token, the platform decides whether to allow it.
Gateway sits between the platform and the worker pool. It routes execution requests to available workers and relays callbacks back in the other direction. It does not make authorization decisions itself. The platform authenticates to the gateway with JWT tokens, and the gateway authenticates to workers with a per-worker nonce.
Worker is a .NET process running inside a Hyper-V isolated container. It hosts plugins and listens on port 9000 for gRPC Execute calls from the gateway. When a plugin needs to talk to Dataverse, the worker proxies the request back up through the same bidirectional gRPC stream. The container runs as ContainerAdministrator with SYSTEM-equivalent privileges, but the Hyper-V boundary is what's supposed to keep that contained.
Sidecar is a host-side gRPC service running outside the Hyper-V container on the same physical node. It provides environment configuration, token acquisition, extension loading, and telemetry across 22 methods. None of them require authentication. No token, no certificate, no API key.
What the Sidecar Returns
We called every sidecar method without any credentials. The headline discovery: GetEnvironmentVariables hands back the Plex_Nonce, four Microsoft tenant IDs, the full cluster configuration, and a telemetry routing table mapping all 43 co-located org IDs to their AppInsights instrumentation keys. Additionally, GetOpenIdSigningKeys returns a full JWKS containing RSA public keys and X.509 certificate chains.
Seven write methods also accepted calls without authentication, covering worker lifecycles, crash events, metric injection, and port proxy creation. We also tried testing the extension-loading methods (`LoadExtensionPackage`, `LoadHostInjectionPackage`, `LoadDataverseExtensionAssembly`). None returned data for any input we tried.
Impact: Cross-Plugin Attacks
Link copied
Plugins from the same organization share a container pool. We confirmed this by registering a second plugin and observing both executing on the same container, sharing a filesystem and process space.
In production, organizations routinely install third-party plugins from Microsoft AppSource, which lists over 665 Dataverse solutions. Because these plugins handle financial data, PII, CRM records, and authentication tokens, having them co-located next to an attacker-controlled plugin with SYSTEM privileges means a single malicious installation can immediately pivot to intercepting legitimate plugin traffic.
We proved this by using the malicious plugin’s ability to debug process memory and set up an interceptor that periodically scans the workers memory, catching the data going in and out of legitimate plugins co-located on the same container pool.
Our PoC injected a unique marker into a payload sent to another legitimate plugin; the malicious plugin scanned for this marker in memory and pulled the surrounding payload data, confirming that we were able to catch the payload in the worker’s memory.
In a real attack—given the worker has full internet access—the interceptor would likely just periodically dump all the worker’s memory to a blob storage for offline analysis.
Figure 9: A malicious plugin can intercept the data of another legitimate plugin co-located on the same container by simply scanning the containers memory.
We also attempted a DLL cache poisoning attack by replacing a legitimate plugin's cached assembly on the shared filesystem with a trojaned version. Alarmingly, the worker loads whatever binary it finds in the cache without re-verifying signatures. Ultimately, SYSTEM on a shared container opens many doors—memory interception and cache poisoning are just two methods among many potential vectors.
Microsoft’s Response
Link copied
After sharing the findings and writeup to Microsoft, they asked we don't refer to this primitive as a "container escape". Their position is that getting SYSTEM on the box via user supplied code is part of their threat model for sandbox containers. Their exact quote from our discussion:
“As discussed, the container is treated as a hostile, multi-tenant environment within our security model.” --MSRC, June 22 2026
However, when Phantom Labs looked through the Dataverse plugin docs (June 5th 2026) we could not seem to find Microsoft’s portrayal of this security model stated anywhere clearly. Rather, their documentation on how to “write custom Azure-aware plugins” stated the following:
Figure 10: Previous Microsoft Plugin document stating “No other external access, such as access to a local file system, is allowed.”
The line “No other external access, such as access to a local file system, is allowed” was dropped sometime after this screenshot was taken. As this was a stated boundary of the security model prior, Phantom Labs stands by the “container escape framing”.
Figure 11: Microsoft’s updated docs which removed: “No other external access, such as access to a local file system, is allowed.”
As part of this research, we have released Plex Explorer, a live website detailing the plex architecture and the tooling we used to probe sandbox containers.
Phantom Labs™ researchers "think like attackers" to expose privilege escalation paths and identity attack vectors, helping defenders proactively uncover misconfigurations and detect emerging identity, cloud, and AI security threats in complex hybrid and cloud environments. Using advanced graph modeling, Phantom Labs researchers map attack paths to privileged access across cloud and on-premises infrastructure.
Simon Maxwell-Stewart is a University of Oxford physics graduate with over a decade of experience in the big data environment. Before joining BeyondTrust, he worked as a Lead Data Scientist in healthcare, and successfully brought multiple machine learning projects into production. Now working as a "resident graph nerd" on BeyondTrust's security research team, Simon applies his expertise in graph analysis to help drive identity security innovation.
Phantom Labs™
BeyondTrust
BeyondTrust Phantom Labs™ believes the best way to fully understand cybersecurity threats is to work closely with our customers and partners, conducting real world research into the attacks that matter most to them. By dissecting emerging attack methods and exploitation techniques of threat actors, as well as conducting novel research, the team’s mission is to help organizations defend against identity threats.
Prefers reduced motion setting detected. Animations will now be reduced as a result.