| Item | Detail |
|---|---|
| CVE ID | CVE-2026-72898 |
| Affected Product | Metabase (open-source / enterprise BI tool) |
| Vulnerability Type | SQL Injection (CWE-89) |
| Auth Required | No (pre-auth) |
| Attack Vector | POST /api/session/reset_password |
| Severity | Critical (CVSS ~9.x) |
| Impact | Admin account takeover, full application DB exposure |
Metabase is a BI platform that provides dashboards and a query builder. It's written in Clojure and assembles its SQL internally through a query-builder library called HoneySQL. This vulnerability comes from the password reset API taking values outside the fields the developers expected and letting them flow, unvalidated, all the way into the query builder.
Here's the one-line version:
If you slip one extra, undeclared field into the request body, that value gets used verbatim as part of the raw SQL string. Since the endpoint requires no authentication, a single request is enough for an attacker to reset the password of any user — including an admin — to whatever they choose.
Where the Bug Lives
The reset_password endpoint is designed to take an emailed token and a new password, then update that user's password. The intended flow looks like this:
- User clicks "Forgot password"
- Server generates a temporary token and emails it
- User calls
reset_passwordwith the token + new password - Server looks up the user by the token, and updates only that user's password
The problem is step 4. Instead of anchoring the update to "the user looked up via the token," there was a code path where the server trusted whatever value showed up in the request body to decide which record to update. And because that value passed through HoneySQL's query builder unvalidated, it hit a spot where it was interpreted not as a bind parameter, but as a raw SQL expression.
Understanding It at the Source Level
Metabase's backend is Clojure, and request bodies are typically pulled apart with destructuring, something like:
(defn reset-password
[{{:keys [token password]} :body}]
;; intended: only pull token and password out of the body
...)
This reads as "only take token and password from the request body." But in the vulnerable version, there was a path that also bound the entire body via :as body, and downstream logic mixed in code that read extra fields straight out of that body map:
(defn reset-password
[{{:keys [token password] :as body} :body}]
;; some downstream helper reaches back into `body`
;; for a key that was never declared in :keys
...)
(Note: the snippets above are reconstructed to illustrate the mechanism based on public technical analysis — they are not a line-for-line copy of Metabase's actual source.)
Two things stand out here:
First, "only declared fields are read" wasn't actually enforced. Clojure's map destructuring is convenient, but binding the whole original map alongside it via :as body does nothing to stop some other piece of code from reaching into body for a key that was never declared. So even if the API spec only documents token and password, if the actual code references another key on body, that value survives and gets passed along.
Second, when that value landed in the :where clause of a HoneySQL query map, it passed through a spot where it was treated as a raw SQL fragment instead of a bind parameter. HoneySQL is safe by default — ordinary values get auto-bound to ? placeholders. But somewhere in the codebase, this value either got wrapped in something like [:raw ...] (or an equivalent helper that treats a string as literal SQL), or passed through a dynamic condition-building utility that carried the incorrect assumption that "this value is already safe SQL." The result: an attacker-supplied string gets executed as part of the actual SQL syntax.
To summarize the attack conditions:
- Request body validation worked by pulling out whatever key happened to be present, rather than enforcing a whitelist of allowed keys.
- The value pulled out was treated as code, not data, at the point the query string was assembled.
Put those two together on an endpoint with no authentication at all, and you get SQL injection.
The Attack, From the Attacker's Side
No special prerequisites needed — just network access to a Metabase instance.
- Confirm the normal
reset_passwordrequest shape (token,password) - Add an extra, undeclared key (e.g., one that functions as a user identifier)
- The server accepts this extra key without validation and hands it to the query builder
- The value gets interpreted as a raw SQL fragment, tampering with the query that decides whose password gets updated
- The password of whatever user the attacker specified — including an admin account — gets changed to a value the attacker controls
- The attacker logs in normally with that account and has full admin access
Once you have admin, you can read credentials for connected data sources, view every piece of data exposed in dashboards, change settings, and create new admin accounts or API keys — effectively full takeover of the instance.
Why This One's Nasty
Typical SQL injection shows up somewhere visible — a login form, a search box, some spot where "user input clearly goes into a query." This case is different:
- Looking only at the documented parameters (
token,password), there's nothing to suggest a vulnerability. The actual bug lived in a field that was never documented — one the developers never explicitly declared as something the endpoint accepts. - Even a query builder that's "designed to be safe," like Clojure/HoneySQL, loses that safety the moment a value passes through even one helper function that treats it as raw SQL.
- Because the endpoint requires no authentication, a successful exploit skips privilege escalation entirely and lands directly on admin access.
The Fix
Two principles were applied together in the patched version:
-
Enforce the input schema as a whitelist. Anything outside the explicitly allowed keys (
token,password) now gets dropped at the destructuring step itself, and the path that let code reach back into the full original map via:as bodywas removed entirely. -
Pin the update target to a server-side lookup, not client input. The user is now looked up by the token first, and only that lookup result's
idis used as the update target — removing any way for the client to specify the update target itself.
Detection and Response Notes
- Check WAF or reverse-proxy logs for
/api/session/reset_passwordrequests whose body contains keys beyondtokenandpassword. - Cross-reference admin password-change history, new account creation, and API key issuance around the relevant time window.
- Until you can upgrade to the patched version, adding an extra layer of request-body schema validation (rejecting anything outside the allowed keys) at the reverse-proxy level can serve as a stopgap mitigation for the
reset_passwordendpoint.


Top comments (0)