Research/wpmgr-dex-default-password
0-dayNo CVECriticalPublic

WPMgr v0.61.199, shipped Dex password on 0.0.0.0

WPMgr v0.61.199 default compose publishes the web UI and Dex on 0.0.0.0 with admin@wpmgr.dev / password and client secret wpmgr-oidc-dev-secret. init-env.sh never rotates Dex. After claim, an unauthenticated caller SSOs and creates a tenant. Labbed. No CVE yet.

Name
WPMgr v0.61.199, shipped Dex password on 0.0.0.0
Type
0-day analysis
CVE
n/a
CVE Risk
critical
Disclosure Status
public
Vendor
mosamlife
Affected
WPMgr v0.61.199 (e1b3a163). Default infra/docker-compose.yml and quickstart-selfhost. Instance must already be claimed. Lab binds loopback only.
Published
09 Nov 2026
Updated
09 Nov 2026
Tags
0-day, wpmgr, default-credentials, cwe-798, cwe-1188, remote, unauthenticated

The identity box came with a password in the box.

WPMgr is a WordPress fleet manager. You compose it up, claim it once, and then it is supposed to be yours. The installer is careful about some secrets. Session key, agent Ed25519, age, database, S3, bootstrap claim. Dex is the sign-in box that ships in the same stack. Dex still has admin@wpmgr.dev / password.

Anyone who can reach the published web port and the Dex port, after that first claim, can sign in through that shipped account and open their own organisation. They do not need the claim secret. They do not need SMTP. They do not need a WPMgr password of their own.

This is WPMgr v0.61.199 (e1b3a163988c9f1c31bc93922ea34322fa188792). No CVE yet. CWE-798 / CWE-1188. 9.8 Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). Remote default credentials. Default compose. Lab and write-up: abraxas/wpmgr-dex-default-password (loopback client).

The product policy is a VDP, no cash table. I did not open a public issue on mosamlife/wpmgr. I did not talk to manage.wpmgr.app.

Why compose, not Helm

Helm on this pin is an empty .gitkeep. The documented self-host path is infra/docker-compose.yml plus scripts/quickstart-selfhost.sh. That path waits on Dex. .env.example leaves WPMGR_OIDC_ISSUER= blank and comments it as email-and-password only. Compose interpolates ${WPMGR_OIDC_ISSUER:-http://dex.localhost:5556/dex}. An empty value counts as unset. Every default boot enables SSO against the shipped Dex.

The API port is bound to 127.0.0.1. Web :8088 and Dex :5556 are not. dex.localhost plus extra_hosts: host-gateway only needed loopback. They got the world.

Pinning v0.61.199

Tag v0.61.199, commit e1b3a163. WordPress.org agent fleet-agent-site-manager sits around a hundred installs. I hunted the Go control plane and the PHP agent. Thirty-two class notes. Four leftovers. This one is the default-credential slot.

The skip list

Grafana anonymous Admin is --profile observability only. SeaweedFS ships wpmgr / wpmgr-dev-secret with Admin on :8333; init-env.sh rewrites that secret on the recommended path. Postgres wpmgr_app is NOSUPERUSER NOBYPASSRLS. First-run ownership stays claim-gated. SignInWithSocial returns registration_closed until an owner membership exists. I did not call those leftovers.

What I built

The lab compose binds 127.0.0.1 only. run.sh refuses to start if the lab file itself publishes a non-loopback bind. The leftover is the product file. poc.py reads infra/docker-compose.yml, infra/dex/config.yaml, scripts/init-env.sh, and .env.example from a clone of the pin and asserts those four facts before it talks to Dex.

Then bootstrap claim 201. Then GET /auth/oidc/login on the web port, Dex password admin@wpmgr.dev / password, callback, wpmgr_session. Then POST /api/v1/orgs. 201. Org c91a0590-f8ea-4af2-bc25-b5e8894ae330.

Wrong turn: I spent a while on whether empty WPMGR_OIDC_ISSUER in the env file would win. Compose environment: with :- wins over env_file. The comment in .env.example is aspirational.

What an attacker can do

Needs a default compose/quickstart stack reachable on the published web and Dex ports, after first-run claim. In plain language: if you followed the install doc and left the ports alone, someone on the network can walk in through the shipped sign-in box and make a tenant next to yours.

Privilege Method / sink Recon / integrity / availability Lab-proven or inferred
Confirm shipped Dex user and secret on the pin infra/dex/config.yaml admin@wpmgr.dev / bcrypt of password, wpmgr-oidc-dev-secret Recon Lab-proven
Confirm web and Dex publish on 0.0.0.0 infra/docker-compose.yml :8088 and :5556 Recon Lab-proven. API stays 127.0.0.1
Confirm init-env.sh never rotates Dex scripts/init-env.sh Recon Lab-proven
SSO as the shipped Dex user GET /auth/oidc/login → Dex password login Integrity (authn) Lab-proven. wpmgr_session
Create a tenant as that session POST /api/v1/orgs Integrity (tenant create) Lab-proven. 201

Stays closed on this leftover

Axis Method / sink Why it is closed
Integrity First-run ownership Claim-gated
Integrity API port Bound 127.0.0.1
Integrity Helm Empty
Integrity Postgres app role NOSUPERUSER NOBYPASSRLS
Integrity SeaweedFS S3 secret init-env.sh rewrites it
Plain text
IOC pin-bind web=0.0.0.0:8088 dex=0.0.0.0:5556 api=127.0.0.1:8081
IOC pin-dex admin@wpmgr.dev bcrypt-password client=wpmgr-oidc-dev-secret
IOC pin-init-env never-rotates-dex
IOC dex-sso=admin@wpmgr.dev session=1
SUCCESS WPMGR-H18-DEX-DEFAULT WPMGR-H18-DEX-CREATED-TENANT

The fix

Bind Dex and the web UI to loopback the way the API already is, or require an explicit publish. Generate the Dex static password and client secret in init-env.sh. Do not enable OIDC when .env leaves the issuer empty. Delete admin@wpmgr.dev from the shipped config.

I am @abraxas_null. Site abraxaslabs.tech. GitHub abraxas. Mail abraxas.null@proton.me.