{"slug": "metabase-cloud-the-winner-takes-it-all", "title": "Metabase Cloud: The winner takes it all", "summary": "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.", "body_md": "With how many bugs AI is starting to find, the hard part is no longer finding them. It is knowing which ones actually matter:\n\n- Which are interesting?\n- Which are devastating?\n- Which ones let an attacker compromise the entire infra?\n\nAt 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.\n\nPreviously 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.\n\n## H2 INIT-imidates again\n\nH2 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.\n\nThis is a known attack, and hence Metabase has preventions for this, one strips `INIT=`\n\nout of the connection string, and one rejects any H2 connection that carries an `INIT`\n\n.\n\nHowever, both checks can be bypassed! They only look for `INIT`\n\ninside the URL. So instead of putting it in the URL, we put it in its own separate field next to the URL.\n\nThe reason it survives is that only the URL ever gets cleaned. The H2 driver runs the sanitizer on the `db`\n\nURL and nothing else:\n\nEvery other field is passed on untouched to the connection builder, which copies it into the connection as-is, including `INIT`\n\n:\n\nThat last line, `dissoc opts :db`\n\n, takes everything except the URL and forwards it as-is, so our `INIT`\n\nfield flows along.\n\nBut we still need to get our malicious H2 database saved, and H2 connections are turned off by default. The normal `POST /api/database`\n\npath runs `can-connect? :h2`\n\n, which throws “H2 is not supported as a data warehouse” before our `INIT`\n\nis 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?`\n\n, so our H2 record (with its `INIT`\n\nfield) gets persisted unchecked. Once it exists, we ask Metabase to sync the database. Sync flips that switch on by itself before opening the connection:\n\nWith being able to run H2, we use an arb file read/write to get RCE. We use two INIT statements.\n\nThe first `INIT`\n\nabuses H2’s `CSVWRITE`\n\nas a plain file-write, dropping our Clojure code at `/tmp/evil.clj`\n\n.\n\nThe second `INIT`\n\npoints a SQL alias at a Java method already loaded in the Metabase process, `clojure.lang.Compiler.loadFile`\n\n, then calls it on the file we just wrote:\n\nRunning against our Metabase Cloud tenant, gives us code execution inside our pod in Metabase Cloud infra.\n\n## Pwning other tenants\n\nThe cloud environment had a chain of misconfigurations that turned our pod into disclosing secrets of every other tenant’s (pods).\n\nThe 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`\n\n.\n\nWith those credentials, EC2, ECR, S3, and EKS describe calls all succeeded, exposing the full production instance inventory and container registry.\n\nWe 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`\n\nKubernetes’ 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.\n\nThe 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.\n\nThese were the tenant pods we got access to:\n\n| Credential | What it grants |\n|---|---|\n| PostgreSQL connection strings | Full read/write to each tenant’s database |\n`MB_ENCRYPTION_SECRET_KEY` | Decrypt all stored Metabase secrets per tenant |\n`MB_API_KEY` | |\n| Site URLs | Identify each customer |\n\nA small sample of the confirmed tenant instances (site URLs and API keys redacted):\n\n| Namespace | Site URL | API Key (truncated) |\n|---|---|---|\n| hosting-94950cc15a15c03e | https://.metabaseapp.com- | mb_api_key_ec191ad1… |\n| hosting-d89f6af7e1ef27fc | https://data.******.com | mb_api_key_9481e01e… |\n| hosting-68e56f67966baef2 | https://**************.metabaseapp.com | mb_api_key_96277d1b… |\n\nFun fact: While doing this recon on a Friday night, this triggered some logging alerts for Metabase team xD\n\n~~With the site URLs + MB_API_KEY in hand we can access the Metabase cloud instance as admin user.~~\n\nWe’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.\n\nThis can be scaled further: with roughly 24 tenants per node and many nodes in the `prod-metabase-1`\n\ncluster, each node compromise yields all of its tenants.\n\n## Conclusion\n\nThis 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.\n\nThe Metabase team was able to patch the underlying connection logic and harden their cloud infrastructure boundaries before malicious actors could exploit the blast radius.\n\nAs 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.", "url": "https://wpnews.pro/news/metabase-cloud-the-winner-takes-it-all", "canonical_source": "https://www.hacktron.ai/blog/metabase-cloud", "published_at": "2026-06-25 00:00:00+00:00", "updated_at": "2026-07-17 21:51:01.782824+00:00", "lang": "en", "topics": ["ai-safety", "ai-infrastructure"], "entities": ["Hacktron", "Metabase", "Supabase", "AWS", "EKS", "H2", "Clojure", "Kubernetes"], "alternates": {"html": "https://wpnews.pro/news/metabase-cloud-the-winner-takes-it-all", "markdown": "https://wpnews.pro/news/metabase-cloud-the-winner-takes-it-all.md", "text": "https://wpnews.pro/news/metabase-cloud-the-winner-takes-it-all.txt", "jsonld": "https://wpnews.pro/news/metabase-cloud-the-winner-takes-it-all.jsonld"}}