Docker Hub GhostPat: when a token changes owner

$ cat blog/docker-hub-ghostpat-token-ownership.md
✓ incident
✓ access control
✓ authentication
✓ containers
6 min read
$ 

Docker has fixed a critical Docker Hub flaw, tracked as CVE-2026-105445 and nicknamed "GhostPat", that let someone create an access token on their own account and then make it act as another user at the registry. The bug was open for about two years, from August 2024 until a fix on September 30, 2026. The Hacker News has the details.

What happened

Docker Hub lets you convert a personal account into an organization. When you do, you name a user as the organization's owner. According to the report, the conversion "transferred the account's personal access tokens (PATs) to the user designated as the organization's owner." The "conversion changed the owner recorded for each token and left its secret unchanged," and "the named owner was not asked to confirm the change and was not notified."

That turned into an attack with two steps:

  1. Create a PAT on your own account. You know its secret.
  2. Convert the account into an organization and name the victim as owner.

The token now worked at the registry as the victim. Within its permissions, it could pull, push or delete images in the victim's repositories and in organizations the victim could access, which, as the report notes, "includes publishing images under a legitimate name." The impact was limited to the registry: no access to the web UI, no password, email or 2FA change, no account takeover.

Key facts from Docker and the report:

  • Rated critical, CVSS 4.0 score 9.4.
  • Open from August 26, 2024 until the fix on September 30, 2026, deployed less than four hours after the report.
  • Docker reviewed about 304,000 pushes made with transferred tokens. All went to the converted organization or its named owner. "We found no evidence that the vulnerability was exploited," Docker said. The report notes Docker did not say what it found for pulls or deletions.
  • Docker notified users on October 2 and deactivated transferred tokens on October 8, except those whose owners asked to keep them.
  • It was found by Peter Molnar and Krisztian Hozak (mptrdev.com).
  • Docker now requires consent before naming an organization owner, notifies the user and logs every token ownership change.
  • A documentation note dated April 22, 2025 described the token transfer. It was removed on September 30.

Why it worked

The registry decided "who is this request from?" by looking up the token's recorded owner at request time. That is a normal design. The problem was that another operation could rewrite the owner field, and nothing tied the token back to the person who actually created it and knew its secret.

A token is a statement: "whoever holds this secret may act as user X, with these permissions." If X can be changed later by someone other than X, then the person who holds the secret, here the account converter, gets to act as whoever they name. The token's secret never changed, so the converter kept using it.

Here is an illustrative example in Python, not Docker's code. The vulnerable version accepts the new owner from the request and moves every token across:

# illustrative example, not the product's code (vulnerable)
@app.post("/accounts/{account_id}/convert")
def convert_to_org(account_id: str, body: ConvertRequest, user=Depends(current_user)):
    account = get_account_owned_by(user, account_id)
    org = create_org_from(account, owner_id=body.owner_id)   # anyone can be named
    db.tokens.update_many(
        {"owner_id": account.id},
        {"$set": {"owner_id": body.owner_id}},             # token now acts as the named owner
    )
    return org

The fixed version keeps tokens bound to their creator, requires the named owner to accept, and records the change:

# illustrative example, not the product's code (fixed)
@app.post("/accounts/{account_id}/convert")
def convert_to_org(account_id: str, body: ConvertRequest, user=Depends(current_user)):
    account = get_account_owned_by(user, account_id)
    invite = create_owner_invite(account, invitee_id=body.owner_id)  # owner must accept
    notify(body.owner_id, "owner_invite", invite.id)
    revoke_tokens(owner_id=account.id, reason="account_converted")   # never re-point them
    audit("org_conversion_requested", actor=user.id, target=body.owner_id)
    return {"status": "pending_owner_acceptance"}

Three general rules come out of it:

  • Bind credentials to who created them. If the subject has to change, revoke the old credential and let the new subject issue their own.
  • Make ownership changes consensual and visible. Ask the new owner, notify them, and log every change, so a transfer can't happen silently.
  • Authorize at use time against the current, legitimate grant, and treat any field that grants authority, such as owner_id, role or org_id, as something the client must never set directly.
attack, as reported where OpenRouting checks 1 Attacker creates a PAT on own accountvendor-side flaw 2 Converts account, names victim owner✓ Access control agent 3 Token owner field silently rewritten✓ API rules: mass assignment 4 Token acts as victim at registry✓ Authentication agent 5 Push image under a trusted name✓ CI/CD agent: unpinned images
The attack as reported, and where an OpenRouting check applies. Illustrative.

What to do now

If you use Docker Hub:

  1. Check Docker's notice if you received one on October 2, and review the tokens on your account and organizations. Revoke any you don't recognize.
  2. Review recent pushes to your repositories for images you didn't publish.
  3. Pin images by digest (image@sha256:...) in Dockerfiles, Compose files and Kubernetes manifests, rather than a tag that a push can move.
  4. Sign images and verify signatures at deploy time, so "published under a legitimate name" isn't enough to be trusted.
  5. Scope tokens tightly. Prefer read-only tokens where you only pull, and short-lived tokens for CI.

If you build a product with accounts, tokens or organizations:

  1. Find every code path that changes an owner, role or organization field on a credential, resource or membership, and check it requires the affected user's consent.
  2. Revoke, don't transfer. On account conversion, merge or deletion, revoke the old credentials.
  3. Never bind ownership fields from the request body. Allowlist the fields a client may set.
  4. Log and notify on every ownership or permission change, so it can be audited and noticed.

How OpenRouting helps

This was a logic flaw in a hosted service, so no scan of your repositories would have found it in Docker Hub. What OpenRouting can do is look for the same class of bug in your own code and API design:

  • The [access control agent](/agents/access-control) reviews findings about authorization: checks that trust ids from the request, and operations that change who owns what without checking the caller's right to do it.
  • The [authentication agent](/agents/authentication-jwt) reviews token and session handling, such as tokens that aren't bound to a subject or aren't revoked when they should be.
  • API collection rules check imported Postman, OpenAPI and other collections for missing auth, guessable IDs and mass assignment, for example an owner_id field accepted in a request body.
  • Semgrep's Dockerfile rules and the container agent flag risky settings in your own Dockerfiles and manifests, and the CI/CD pipeline agent calls out pipeline images and actions not pinned by SHA. Our post on Dockerfile security mistakes covers the common ones.
  • AI triage (Claude, via Amazon Bedrock) gives each finding a verdict, a plain-English explanation and a suggested fix, and PR review runs the same checks on the files a pull request changes.

What it won't catch

  • Business-logic flaws are hard for any scanner. A token transfer that "works as designed" doesn't match a pattern. Our findings and triage are prompts for a design review, not proof that ownership logic is sound. The broken access control post has a two-user test worth running by hand.
  • Your registry. OpenRouting doesn't monitor Docker Hub or any registry, your tokens, or who pushed what.
  • Vendor services. It scans your repositories and, passively, your websites, not a provider's infrastructure.
  • Findings need a scan. Results appear when you run a scan or review a pull request.

Sources

Questions

What was the Docker Hub GhostPat flaw, CVE-2026-105445?

Converting a Docker Hub personal account into an organization transferred the account's personal access tokens to the user named as the organization's owner, without asking or notifying that user. An attacker could create a token, convert the account naming a victim as owner, and then use the token at the registry as the victim.

Was GhostPat exploited?

Docker reviewed about 304,000 pushes made with transferred tokens and said it found no evidence that the vulnerability was exploited. The report notes Docker did not say what it found for pulls or deletions.

What should Docker Hub users do after GhostPat?

Check Docker's notice if you received one on October 2, review the tokens on your account and organizations, and revoke any you don't recognize. Review recent pushes for images you didn't publish, pin images by digest and verify signatures at deploy time.

How do I avoid the same ownership bug in my own product?

Keep credentials bound to the person who created them: on account conversion, merge or deletion, revoke tokens instead of re-pointing them. Require the new owner's consent for ownership changes, notify them, log every change, and never bind fields such as owner_id from the request body.

Keep reading