Metabase Cloud: The winner takes it all Hacktron researchers discovered critical vulnerabilities in Metabase Cloud that could have allowed attackers to compromise all cloud tenants. By exploiting flaws in H2 database connection handling and Kubernetes node authorizer misconfigurations, they achieved remote code execution and accessed secrets of 24 tenant pods, including database credentials and AWS keys. With how many bugs AI is starting to find, the hard part is no longer finding them. It is knowing which ones actually matter: - Which are interesting? - Which are devastating? - Which ones let an attacker compromise the entire infra? At Hacktron be it research or an AI pentest our goal is to find the bugs that can completely compromise companies, and helping secure them before attackers do. Previously we wrote SupaPwn https://www.hacktron.ai/blog/supapwn showing how we got Supabase tenants access. This time it is Metabase, where we could have pwned every cloud tenant. H2 INIT-imidates again H2 is an embedded Java database that Metabase ships by default, and it is not a plain data store. Its SQL can read and write files on the host and call Java methods inside the Metabase process, so making H2 run our SQL is effectively an RCE primitive on the server. This is a known attack, and hence Metabase has preventions for this, one strips INIT= out of the connection string, and one rejects any H2 connection that carries an INIT . However, both checks can be bypassed They only look for INIT inside the URL. So instead of putting it in the URL, we put it in its own separate field next to the URL. The reason it survives is that only the URL ever gets cleaned. The H2 driver runs the sanitizer on the db URL and nothing else: Every other field is passed on untouched to the connection builder, which copies it into the connection as-is, including INIT : That last line, dissoc opts :db , takes everything except the URL and forwards it as-is, so our INIT field flows along. But we still need to get our malicious H2 database saved, and H2 connections are turned off by default. The normal POST /api/database path runs can-connect? :h2 , which throws “H2 is not supported as a data warehouse” before our INIT is ever reached, so nothing gets stored. The way in is the EE serialization import endpoint: it builds Database records straight from the uploaded YAML and never calls can-connect? , so our H2 record with its INIT field gets persisted unchecked. Once it exists, we ask Metabase to sync the database. Sync flips that switch on by itself before opening the connection: With being able to run H2, we use an arb file read/write to get RCE. We use two INIT statements. The first INIT abuses H2’s CSVWRITE as a plain file-write, dropping our Clojure code at /tmp/evil.clj . The second INIT points a SQL alias at a Java method already loaded in the Metabase process, clojure.lang.Compiler.loadFile , then calls it on the file we just wrote: Running against our Metabase Cloud tenant, gives us code execution inside our pod in Metabase Cloud infra. Pwning other tenants The cloud environment had a chain of misconfigurations that turned our pod into disclosing secrets of every other tenant’s pods . The EC2 Instance Metadata Service was accessible from inside the pod, which gives us temporary AWS credentials for the node’s IAM role, eks-cluster-prod-metabase-1-nodes . With those credentials, EC2, ECR, S3, and EKS describe calls all succeeded, exposing the full production instance inventory and container registry. We used the obtained node credentials to mint an EKS bearer token and authenticate to the cluster API as - system:node:ip-10-241-27-69.ec2.internal Kubernetes’ node authorizer intentionally lets a node read the specs and secrets of pods scheduled on it. The security model assumes that compromising a node already means compromising its pods. Hence the node identity we just got allows us to read all other pods tenants and their secrets. The node was running 24 tenant Metabase pods, each in its own namespace. Every pod spec carries its environment variables, and the node authorizer let us read all of them. These were the tenant pods we got access to: | Credential | What it grants | |---|---| | PostgreSQL connection strings | Full read/write to each tenant’s database | MB ENCRYPTION SECRET KEY | Decrypt all stored Metabase secrets per tenant | MB API KEY | | | Site URLs | Identify each customer | A small sample of the confirmed tenant instances site URLs and API keys redacted : | Namespace | Site URL | API Key truncated | |---|---|---| | hosting-94950cc15a15c03e | https://.metabaseapp.com- | mb api key ec191ad1… | | hosting-d89f6af7e1ef27fc | https://data. .com | mb api key 9481e01e… | | hosting-68e56f67966baef2 | https:// .metabaseapp.com | mb api key 96277d1b… | Fun fact: While doing this recon on a Friday night, this triggered some logging alerts for Metabase team xD ~~With the site URLs + MB API KEY in hand we can access the Metabase cloud instance as admin user.~~ We’ve made a mistake in our initial assumption due to lack of testing of the obtained env vars of the pod. The MB API KEY doesn’t grant access to the instance. However other env vars including database credentials did persist though were not tested on the env as it would be unauthorized, per Metabase team those would not be useful in gaining further access to the system. This can be scaled further: with roughly 24 tenants per node and many nodes in the prod-metabase-1 cluster, each node compromise yields all of its tenants. Conclusion This chained exploitation highlights that a single sanitization oversight can cascade into a total multi-tenant cluster compromise. Security for such systems cannot rely on single-point defenses, especially when complex third-party drivers or legacy protocols are involved. The Metabase team was able to patch the underlying connection logic and harden their cloud infrastructure boundaries before malicious actors could exploit the blast radius. As AI-driven testing continues to uncover hidden entry points, building resilient, defense-in-depth architectures is the only way to ensure that a compromise at the application layer doesn’t equal a compromise of the entire cloud.