Metabase Unauthenticated SQLi: Detection & Response Checklist (CVE-2026-72898)
Self-hosted Metabase, version x.58.0 through x.63.4, has an unauthenticated SQL injection in POST /api/session/reset_password that writes straight into the application database (the one holding accounts, connections, and settings, not your warehouse). An attacker who reaches that endpoint gets admin of the instance plus the ability to use the stored credentials for every database you connected. It is in the CISA KEV catalog (dateAdded 2026-08-11, dueDate 2026-08-14) and is under active exploitation.
Work through this top to bottom. Steps 1 and 2 tell you whether you are affected and exposed. Steps 3-6 look for signs you were already hit. If the answer to any step is "yes," treat it as a compromise, not a near miss, and rotate everything in Step 7.
1. Check your version
The fixed releases are x.58.24, x.59.21, x.60.17, x.61.11, x.62.9, x.63.5 (per GHSA-vwf4-m7j8-wcjf). Anything on an affected major line below its patched point is vulnerable.
Docker, pinned image tag:
# Image the container was started from (shows the tag you pinned)
docker inspect --format '{{.Config.Image}}' metabase
# Or list running Metabase containers with their image
docker ps --filter "ancestor=metabase/metabase" --format '{{.Names}}\t{{.Image}}'
Docker, latest tag (the tag lies; read the version from the startup log):
docker logs metabase 2>&1 | grep -m1 "Starting Metabase version"
# -> Metabase version v0.62.13 (abcdef1)
JAR install:
java -jar metabase.jar version
# or, from a running instance, the same banner line in the service log
Compare against the table. Below your line's patched point = vulnerable.
| Major line | Affected | Fixed |
|---|---|---|
| 58 | 58.0 – 58.23 | 58.24 |
| 59 | 59.0 – 59.20 | 59.21 |
| 60 | 60.0 – 60.16 | 60.17 |
| 61 | 61.0 – 61.10 | 61.11 |
| 62 | 62.0 – 62.8 | 62.9 |
| 63 | 63.0 – 63.3 | 63.5 |
Note: /api/health only returns {"status":"ok"}. It does not expose the version, so use the log line or the image tag above.
2. Check internet exposure (port 3000 behind a reverse proxy)
The bug needs the /api/session/reset_password route reachable from the internet without authentication. A reverse proxy in front does not fix this on its own if it forwards the path.
Is port 3000 bound to all interfaces, or loopback only?
ss -tlnp | grep ':3000'
# 0.0.0.0:3000 -> reachable on every NIC (exposed unless the proxy/VPC stops it)
# 127.0.0.1:3000 -> only local; a proxy on the same host is the only path in
Reach the endpoint from outside the host (a different machine, or a phone on a non-local network). A non-destructive probe: check the HTTP status the route returns, not the exploit.
# From OUTSIDE the network. Expect 405 (method-not-allowed) if the route is reachable unauth.
# 404/403 means the proxy is not forwarding it. A 200/400 also means it's reachable.
curl -s -o /dev/null -w '%{http_code}\n' https://your-host.example.com/api/session/reset_password
Also grep the proxy config for the path, and confirm no auth layer sits in front of it:
# Caddy / nginx / traefik: is /api/session/ forwarded, and is there a BasicAuth/OAuth guard?
grep -rn "reset_password\|/api/" /etc/caddy/ /etc/nginx/ 2>/dev/null
If it is exposed, stop it now. This is the vendor's stated workaround and it is safe to do before or instead of upgrading. In Caddy:
@mb_reset path /api/session/reset_password
handle @mb_reset {
abort
}
In nginx:
location = /api/session/reset_password {
return 404;
}
3. Find the application database
Steps 4-6 query the app database (the DB Metabase uses for its own state). It is usually a Postgres you provisioned separately. Find its connection from the compose file, config.yml, or the MB_DB_* environment variables.
# Where does Metabase point its app DB?
grep -iE "MB_DB_|metabase.*postgres|MB_DB_CONNECTION_URI" docker-compose.yml config.yml .env 2>/dev/null
Then connect. Replace <host>, <port>, <db>, <user> with your values. The table names below are the same on Postgres and MySQL/MariaDB.
psql "postgresql://<user>@<host>:<port>/<db>"
# MySQL: mysql -h <host> -P <port> -u <user> -p <db>
4. Indicator: new or changed admin accounts
-- Who is an admin, and when did they join / last log in?
SELECT id, email, first_name, last_name, date_joined, last_login
FROM core_user
WHERE is_superuser = true
ORDER BY date_joined DESC;
-- Login events: new IP addresses or device strings you do not recognize.
-- (May be empty if login-history logging was not enabled on your build.)
SELECT timestamp, user_id, ip_address, device_description
FROM login_history
ORDER BY timestamp DESC
LIMIT 50;
Any admin you did not create, or a date_joined inside the incident window, is a confirmed breach marker.
5. Indicator: modified data-source connections
Connection settings live in metabase_database. The details column stores the credentials as encrypted JSON, so you cannot read the plaintext password from here. What you can see is when a connection was last touched. A recent updated_at you do not explain means someone edited that data source.
SELECT id, name, engine, updated_at, created_at
FROM metabase_database
ORDER BY updated_at DESC;
Cross-check against each connected warehouse's own audit log for access from your Metabase service account in the same window. The SQLi does not hand over a readable password; it hands an attacker admin of Metabase, which they can then use to make Metabase use those connections and pull data out. That is where the warehouse-side logs matter.
6. Indicator: unexpected exports and new API keys
Large or dump-shaped queries in the execution log are the export signature. The query_execution table is populated on the open-source build.
-- Big result sets or raw-native queries that look like extraction.
SELECT id, started_at, executor_id, native, result_rows, context
FROM query_execution
WHERE result_rows > 10000
ORDER BY started_at DESC
LIMIT 50;
-- API keys created recently. Any you do not recognize = compromise.
SELECT id, name, created_at, user_id, group_id
FROM api_key
ORDER BY created_at DESC;
-- Active sessions that should not exist (the vendor remediation step).
SELECT s.created_at, u.email
FROM core_session s
LEFT JOIN core_user u ON u.id = s.user_id
ORDER BY s.created_at DESC;
7. If anything above is a "yes": contain, then clean up
Contain first:
- Take the endpoint down or the instance offline if you are not yet on a patched version.
- Rotate the credentials of every connected database, not just the ones that look touched.
- Revoke all active sessions and delete unrecognized API keys.
Then, on a patched instance, apply the vendor's post-upgrade hardening (GHSA-vwf4-m7j8-wcjf):
- Delete all rows in
core_sessionto revoke every active user session. - Review and delete any API keys you do not recognize.
- Review administrator accounts for unexpected changes.
- Rotate credentials for all connected databases.
- Review the data-warehouse logs for unauthorized access.
- Review Metabase activity and query history for unexpected or unauthorized queries.
Upgrade to your major line's patched release (Step 1 table) before any of the cleanup, because the vulnerable endpoint is still reachable until you patch or block it.
Sources
- GHSA-vwf4-m7j8-wcjf (GitHub advisory, published 2026-08-06): affected/patched versions, CVSS 10.0, remediation list, and the
/api/session/reset_passwordworkaround. - CISA KEV catalog v2026.08.21, entry CVE-2026-72898:
dateAdded 2026-08-11,dueDate 2026-08-14, CWE-89. - Metabase app-database schema (Liquibase changelogs):
core_user,metabase_database,core_session,api_key,query_execution,login_historytable and column definitions.