click the screen · press Enter
← back to blog
Threats & Exploits · Topic 4

A JSON Key Metabase Never Asked For Became a CVSS 10 SQL Injection

CVE-2026-72898 Metabase unauthenticated SQL injection in the password reset endpoint - Threats and Exploits

Every secure coding guide tells you the same thing about SQL injection: use parameterised queries and you're done. Metabase used a query builder. It still shipped a CVSS 10.0 unauthenticated SQL injection, and somebody used it against Metabase Cloud before anyone knew the bug was there. That advice is what I want to argue with. Parameterisation protects you from an attacker-controlled string. It does nothing when the attacker gets to hand you part of the query structure, and CVE-2026-72898 is the cleanest example of that I've seen in a long while.

If you run a self-hosted Metabase somewhere behind a reverse proxy, wired up to the warehouse that holds everything your company actually cares about, this one's for you. That's an extremely common shape and it's the exact shape this bug was built to eat.

CVE-2026-72898 Metabase SQL injection compared before and after the patch: an undeclared user-id key survives a Clojure merge and reaches HoneySQL as a raw SQL directive, versus the patched version that requires a positive integer
One extra key in the JSON body, and a password reset turns into arbitrary SQL.

What happened

On 3 August 2026 Metabase spotted attacks against Metabase Cloud that used a bug nobody had a name for yet. Three days later, on 6 August, they published the advisory (GHSA-vwf4-m7j8-wcjf) and shipped patched builds across every supported branch. The flaw got the ID CVE-2026-72898 and a CVSS 3.1 base score of 10.0, vector AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H. Ten out of ten is rare, and it's usually a sign that the scoring hit every worst case at once: reachable over the network, easy, no login, no clicking, and it breaks out of the thing it lives in.

The vulnerable endpoint is POST /api/session/reset_password. That endpoint has to be unauthenticated, because the whole point of a password reset is that you cannot log in. It expects two fields: a token and a password. Send it a third field it never documented, shaped the right way, and you can run SQL of your choosing against the Metabase application database. From there you can promote yourself to admin, and admin on a business intelligence tool is admin over every database that tool is plugged into.

Affected & fixed builds · GHSA-vwf4-m7j8-wcjf
BranchAffectedFixed build
Metabase x.58≥ x.58.0, < x.58.23x.58.24
Metabase x.59≥ x.59.0, < x.59.20x.59.21
Metabase x.60≥ x.60.0, < x.60.16x.60.17
Metabase x.61≥ x.61.0, < x.61.10x.61.11
Metabase x.62≥ x.62.0, < x.62.8x.62.9
Metabase x.63≥ x.63.0, < x.63.3x.63.5
The x is 0 for open source and 1 for Enterprise, so 0.58.24 and 1.58.24 are the same fix.
A wrinkle worth naming Look closely at the x.58 row. The advisory's affected range stops below x.58.23, but the fixed build it lists is x.58.24. Those two numbers don't quite meet, and the same one-off gap shows up on the other branches. I don't know the reason and I'm not going to guess at one. The safe reading is simple: go to the build Metabase actually names as fixed, not to the first version above the affected range.

The week, in order

This one moved fast, and the order matters if you're trying to work out how long your window of exposure was.

Disclosure to KEV in eight days
01
3 Aug

Metabase finds attacks against Metabase Cloud using an unknown bug, and starts containing it.

02
6 Aug

Advisory published, patched releases out across x.58 to x.63, incident disclosed.

03
7-10 Aug

Affected companies start publishing their own notices. Wiz observes public exploits by 10 Aug.

04
11 Aug

CISA adds CVE-2026-72898 to the KEV catalogue.

Dates from Metabase's advisory, the Wiz Research write-up and CISA's KEV entry.

Wiz Research listed the downstream notices as they landed: Framework and Tally on 7 August, n8n on 8 August, Kilo Code (Anaconda) on 9 August, ChecklyHQ on 10 August. That's a decent reminder that "we use a hosted BI tool" is a supply chain statement, not an IT detail.

The bit that gets me is the four days between the patch dropping and public exploits appearing. Metabase did the right thing, shipped fixes across six branches at once, and self-hosted operators still had a long weekend to lose. Patch windows are getting shorter every year and I don't think most teams have adjusted.

Root cause: four small behaviours that shouldn't have met

Metabase didn't publish the patch diff in the public repository, so the root cause came from reverse engineering. Rami McCarthy and Wiz Research did that work: they pulled the vulnerable and patched JARs (v0.58.22 and v0.58.24), diffed them, decompiled the relevant Clojure class and matched the bytecode back to the open source. Everything in this section is their finding, and I'm just walking through it because it's a genuinely lovely bug.

Metabase's backend is Clojure. Four ordinary, individually reasonable behaviours line up here, and the bug lives in the gaps between them.

How an unknown key becomes SQL
01
merge keeps strangers

The code does (merge request (authenticate ...)). Clojure's merge lets the second map overwrite the first, but it never strips extra keys. Failed auth returns no :user-id, so yours survives.

02
JSON becomes keywords

Metabase keywordises incoming JSON. {"user-id": {"raw": "..."}} parses into the Clojure map {:user-id {:raw "..."}}. Standard behaviour, no bug yet.

03
HoneySQL sees a directive

HoneySQL, the query builder, treats :raw as "put this literal SQL in, skip parameterisation". It's a deliberate escape hatch for developers.

04
The lookup compiles it

That value goes to t2/select-one ... :id user-id. Instead of an integer it gets a :raw map, and the query compiles with your SQL welded into it.

Root cause as reverse engineered and published by Wiz Research.

Read that chain again and notice something: not one of those four behaviours is a bug. merge is doing exactly what the docs say. Keywordising JSON is normal. HoneySQL's :raw exists because sometimes you genuinely need to write SQL the builder can't express. The lookup by ID is the most boring line of code in the file.

Parameterisation stops a hostile string. It has nothing to say about a hostile data structure, because by the time the builder sees one, the attacker is already writing the query with you.

The patch is small and it tells you exactly where the trust boundary should have been. Wiz reconstructed it as roughly this shape: before resolving a user, check that user-id is a positive integer, and if it isn't, refuse and log it. That's the whole fix. Validate the type, at the edge, before anything downstream gets a chance to be clever.

POST /api/session/reset_password takes exactly two fields: token (the reset token from the email) and password (the new one). Nothing else is part of the API.
The same request with one undeclared field added: user-id, whose value is a nested object rather than a number. Nothing rejects it, and the object is what carries the payload.
The gap between the documented API and the accepted API is the whole vulnerability.

I'll admit where I started out wrong. When I first read "SQL injection in a password reset", I assumed the bug had to be in how the reset token was compared or looked up, because that's where these usually live. I spent a while reading around that idea before the Wiz write-up landed and showed me the token was never the interesting part. The token is a red herring. Auth failing is what makes the attack work, because a failed authentication returns a map with no :user-id in it, which is precisely why the attacker's one survives.

Why the blast radius is worse than "one app got popped"

Metabase is a business intelligence tool. Its job is to hold connection details for your databases and warehouses so that people can point and click at charts instead of writing SQL. That job description is also a very good description of a credential vault that nobody treats like one.

  • It's pre-auth. No account, no token, no user interaction. If the endpoint is reachable, the attack is available. That's the whole ballgame.
  • Admin is one hop away. Injecting into the application database means altering application records, and Metabase's own advisory says the result is administrator access.
  • The connected databases are the real prize. Bishop Fox made this point plainly: a compromised Metabase is a springboard into the systems it queries, because it already holds the credentials and the network path.
  • Exposure is common. Wiz reported it sees self-hosted Metabase in roughly 13% of cloud environments, with about a quarter of those fully internet accessible, and around 2,500 instances visible in Shodan.
  • The payload varies by backend. Metabase defaults to H2 but recommends PostgreSQL, MySQL or MariaDB for production, and the injection differs per engine. Handy for attackers who bother to fingerprint, awkward for anyone writing a single detection rule.

Detection and hunting

The nice thing about this bug from a defender's point of view is that the attack has a very distinctive shape. Real clients never send that field.

  • Hunt the undeclared field. Any request body to /api/session/reset_password containing a user-id key is hostile by definition. It isn't part of the API. If your proxy or WAF logs request bodies, that's a single high-confidence string to grep for.
  • Hunt the type. Even where user-id might appear legitimately elsewhere, a user-id whose value is an object rather than a number is the tell. Look for "user-id":{ rather than "user-id":123.
  • Volume on a quiet endpoint. Password resets are rare and human-paced. A burst of POSTs to that path from one source, especially with near-identical bodies, is a blind injection being walked one character at a time.
  • Application database logs. Blind SQLi against the app database will leave unusual query patterns behind. If you log slow queries or statements on the Metabase application database, that's where the injected SQL shows up in its compiled form.
  • Look for the result, not just the attempt. New administrator accounts, unexpected privilege changes, altered email addresses on existing admins, and API keys nobody recognises. Those are the outcomes worth alerting on even if you never see the request.

Version fingerprinting is straightforward too. Wiz pointed at the properties endpoint for working out what you're running:

curl -s https://metabase.example.com/api/session/properties
[INFO] the JSON response carries the running version string compare it against the fixed builds in the table above
Expected behaviour, not captured from my run. I don't have a Metabase instance in the lab to point this at.
Scope note Wiz published a defensive check that adds the offending key with a placeholder where the SQL would go, so teams can confirm their own exposure. I'm deliberately not reproducing a working payload here. Run checks against systems you own or are authorised to test, and nothing else.

The one control that would have broken this earliest

Patching is the answer now, but the interesting question for an autopsy is which single control would have stopped the chain furthest to the left. For me it isn't a WAF rule and it isn't network segmentation, useful as both are.

It's rejecting undeclared fields at the API boundary. A strict schema on that endpoint, one that accepts token and password and throws away anything else, kills this before any of the four behaviours downstream even get a turn. The merge can't preserve a key that was never allowed in. HoneySQL never sees a map. The whole chain needs step one to succeed, and step one is just "the API tolerated a field it didn't ask for".

That's a design habit rather than a product you buy, which is probably why it keeps not happening. Most frameworks default to permissive parsing because strict parsing breaks clients, and "be liberal in what you accept" is one of the oldest bits of advice in networking. It's also, on a security boundary, mostly wrong.

Defender checklist Find every Metabase instance → check the version at /api/session/properties → upgrade to the fixed build for your branch → if it was exposed, revoke sessions and rotate connected database credentials → hunt user-id in reset_password bodies and review admin accounts.

Remediation

  • Upgrade. Move to x.58.24, x.59.21, x.60.17, x.61.11, x.62.9 or x.63.5, whichever matches your branch. That's the fix.
  • If you can't upgrade today, block the endpoint. Metabase's own interim advice is to temporarily block /api/session/reset_password at the proxy. Your users lose self-service password reset until you patch, which is a fair trade for a few hours.
  • Treat an exposed instance as compromised until proven otherwise. Metabase's post-upgrade guidance is specific: revoke all active sessions, review API keys and delete anything unrecognised, check administrator accounts for unexpected changes, rotate credentials for every connected database, and review both warehouse logs and Metabase's own query history.
  • Then ask why it was reachable. A BI tool holding warehouse credentials rarely needs to be on the public internet. If yours is, that decision deserves a second look now that you have a reason to have the conversation.

MITRE ATT&CK mapping

  • T1190 · Exploit Public-Facing Application (the unauthenticated reset endpoint).
  • T1098 · Account Manipulation (altering application records to gain administrator access).
  • T1552 · Unsecured Credentials (stored connection credentials for the databases Metabase queries).
  • T1213 · Data from Information Repositories (Metabase is quite literally the repository).

References

FAQ

What is CVE-2026-72898?

An unauthenticated SQL injection in Metabase's POST /api/session/reset_password endpoint, tracked as GHSA-vwf4-m7j8-wcjf and scored CVSS 3.1 10.0. An attacker who can reach the endpoint can inject SQL into the Metabase application database and end up with administrator access. Metabase confirmed it was exploited in the wild.

Why didn't parameterised queries stop this SQL injection?

Because the attacker supplied query structure, not a string. Metabase builds queries with HoneySQL, which treats a map shaped like {:raw "..."} as a request to embed literal SQL. Parameterisation protects string values. It cannot protect a builder that was handed a directive instead of data.

Which Metabase versions fix CVE-2026-72898?

Metabase lists x.58.24, x.59.21, x.60.17, x.61.11, x.62.9 and x.63.5 as the patched builds, where x is 0 for the open-source edition and 1 for Enterprise. Anything on the x.58 to x.63 branches below those numbers should be treated as vulnerable and upgraded.

Is Metabase Cloud affected or only self-hosted instances?

The original attack was against Metabase Cloud, which Metabase disclosed on 6 August 2026 and patches itself. The ongoing risk sits with self-hosted deployments, because nobody upgrades those but you. Wiz Research reported that around a quarter of the self-hosted instances it sees are fully internet accessible.

If my Metabase instance was exposed, is patching enough?

No. Metabase's own guidance is to revoke all active sessions, review and delete unrecognised API keys, check administrator accounts for unexpected changes, rotate credentials for every connected database, and review warehouse logs and Metabase query history. Patching stops the next attacker, not the one who already came through.