Verdict: the root cause is confirmed, while related end-to-end exploitation is not reproducible against a standard UserDatabaseRealm deployment. Patch anyway.


Primer: HTTP DIGEST Authentication

Before diving into the vulnerability, it is worth understanding how DIGEST authentication works and how Tomcat implements it — because both are directly relevant to why the exploit fails.

How DIGEST Auth Works

HTTP DIGEST authentication (RFC 2617) was designed to avoid transmitting passwords in clear text over the wire, unlike HTTP Basic authentication. Instead of sending the password directly, the client and server perform a challenge-response handshake:

Client                                    Server
  │                                          │
  │── GET /protected/ ────────────────────────►│
  │                                          │
  │◄── 401 Unauthorized ───────────────────────│
  │    WWW-Authenticate: Digest              │
  │      realm="UserDatabase",               │
  │      nonce="<random>",                   │
  │      qop="auth",                         │
  │      opaque="<token>"                    │
  │                                          │
  │  Client computes:                        │
  │    A1 = MD5(username:realm:password)     │
  │    A2 = MD5(method:uri)                  │
  │    response = MD5(A1:nonce:nc:           │
  │               cnonce:qop:A2)             │
  │                                          │
  │── GET /protected/ ───────────────────────► │
  │   Authorization: Digest                  │  Server recomputes A1
  │     username="user",                     │  using its stored password,
  │     response="<computed hash>"           │  then compares digests.
  │                                          │
  │◄── 200 OK ──────────────────────────────────│

The critical element is A1: it is derived from username:realm:password. If the server and client compute the same A1 — and therefore the same final response hash — authentication succeeds.

The realm value serves two purposes: it is included in the WWW-Authenticate challenge so the client knows which credentials to use, and it is baked into the A1 computation so that credentials cannot be replayed across different realms.

The nonce is a server-generated random value included in every challenge. It prevents replay attacks — each request must include a fresh nonce-based computation. The opaque is a server-assigned token that must be echoed back verbatim by the client on every subsequent request within the same authentication session.

The Realm in Tomcat

A Realm in Tomcat is a “database” of usernames and passwords that identify valid users of a web application (or set of web applications), plus an enumeration of the list of roles associated with each valid user. Tomcat defines a Java interface (org.apache.catalina.Realm) that can be implemented by plug-in components to connect to different authentication backends.

The default installation of Tomcat ships with a UserDatabaseRealm configured at the Engine level, backed by conf/tomcat-users.xml. This is the configuration used in our test environment.

DIGEST Auth and Stored Passwords

When using digested passwords with DIGEST authentication, the cleartext used to generate the digest is different: the digest must use one iteration of the MD5 algorithm with no salt, and {cleartext-password} must be replaced with {username}:{realm}:{cleartext-password}. The value for {realm} is taken from the <realm-name> element of the web application’s <login-config>.

This is a key constraint: for DIGEST authentication to work, the Realm must have access to the user’s cleartext password at verification time, because it needs to recompute A1 server-side. This is why getPassword(username) is called inside getDigest() — and why returning null for an unknown user is the root of CVE-2026-43512.

How Tomcat Configures DIGEST Auth

In web.xml, DIGEST authentication is declared via <login-config>:

<login-config>
  <auth-method>DIGEST</auth-method>
  <realm-name>UserDatabase</realm-name>
</login-config>

The <realm-name> value is what populates the realm= field in the WWW-Authenticate challenge. It must match the realm name Tomcat uses internally — in a default installation, this is UserDatabase. A mismatch causes validate() in DigestAuthenticator to reject the response immediately, before even reaching the digest comparison.


Background

On May 1 2026, the Apache Tomcat security team published a fix for a vulnerability in the HTTP DIGEST authentication mechanism. The advisory assigned a CRITICAL severity and described an authentication bypass affecting several versions.


The Vulnerability in getDigest()

HTTP DIGEST authentication (RFC 2617 / RFC 7616) requires both the client and the server to compute the same hash — called A1 — from username:realm:password. If the hashes match, the user is authenticated.

In RealmBase.getDigest(), Tomcat computes the server-side A1. The vulnerable code:

// RealmBase.java — vulnerable (commit sha vulnerable implementation dd5f7b83370b4bb6afdc8dbf4bfa9e29b054370a)
protected String getDigest(String username, String realmName, String algorithm) {
    if (hasMessageDigest(algorithm)) {
        return getPassword(username);  // returns null for unknown users
    }

    // null is silently converted to "null" by Java string concatenation
    String a1 = username + ":" + realmName + ":" + getPassword(username);
    return HexUtils.toHexString(
        ConcurrentMessageDigest.digest(algorithm, a1.getBytes(...))
    );
}

When username does not exist in the configured Realm, getPassword(username) returns null. Java’s string concatenation operator silently converts null to the four-character literal "null". The server therefore computes:

A1 = MD5("ghost:UserDatabase:null")

A client that submits a DIGEST response computed with "null" as the password produces an identical hash.

The Official Fix

The patch (commit 6565a6c) adds an explicit null guard:

// RealmBase.java — patched
protected String getDigest(String username, String realmName, String algorithm) {
    String password = getPassword(username);

    if (password == null) {
        return null;
    }
    ...
}

The PoC Environment

To verify the advisory’s claims, I built a minimal reproducible environment:

  • Container: Tomcat 11.0.0-M1, single protected resource at /protected/secret.html, DIGEST authentication via UserDatabaseRealm
  • Exploit: Go implementation of the RFC 2617 handshake

The full source is at github.com/covepseng/cve-2026-43512-poc.

# Start the container
podman build -t tomcat-cve-2026-43512 .
podman run -d --name tomcat-vuln -p 8080:8080 tomcat-cve-2026-43512

# Run the exploit
go run exploit.go \
  -target   http://localhost:8080 \
  -path     /protected/secret.html \
  -username ghost

Step 1 — The DIGEST Challenge

The first unauthenticated request returns a 401 with a WWW-Authenticate header:

[1] Sending unauthenticated request...
[+] 401 received. Challenge:
    Digest realm="UserDatabase", qop="auth",
           nonce="1780673187224:c5248bfceb68684ca0aabdd1bcdd2b4b",
           opaque="D9F12000CEF8BCE40EAABA18C41B3273"

Step 2 — Computing the Digest with password="null"

The exploit replicates the server’s buggy A1 calculation:

// exploit.go — replicating the server's buggy A1
a1 := md5hex(username + ":" + realm + ":" + "null")
//                                           ^^^^^
// "null" is the Java string representation of a null pointer.
// getPassword("ghost") returns null → Java concatenates it as "null".
// We compute the same value on the client side.

Step 3 — What Tomcat’s Verbose Log Shows

Enabling FINE-level logging in Tomcat reveals the internal state:

Attempting to authenticate user [ghost] with realm [UserDatabaseRealm]
Digest:        2388e2c78407def640f37f092a8d3a84   ← client
Server digest: 2388e2c78407def640f37f092a8d3a84   ← server
Failed to authenticate user [ghost]

The digest hashes are identical. The vulnerability in getDigest() is real and confirmed. Yet authentication still fails.


Why the Bypass Fails: getPrincipal()

Reading RealmBase.authenticate() in full reveals a two-phase check:

String digestA1 = getDigest(username, realm, algorithm);
if (digestA1 == null) { return null; }   // Phase 1: digest check

// ... compute serverDigest from digestA1 ...

if (serverDigest.equals(clientDigest)) {
    return getPrincipal(username);        // Phase 2: principal lookup
}
return null;

Even when the digests match, the method calls getPrincipal(username). In UserDatabaseRealm, this looks up the username in the in-memory database loaded from tomcat-users.xml. For ghost — a user that does not exist — it returns null. The caller treats a null Principal as an authentication failure and issues a second 401.

This is the second, independent defensive layer that prevents end-to-end exploitation in a standard deployment.

The Full Flow

Client → GET /protected/secret.html
  ↓
DigestAuthenticator.doAuthenticate()
  ↓
realm.authenticate("ghost", clientDigest, ...)
  ↓
getDigest("ghost", "UserDatabase", "MD5")
  │  getPassword("ghost") → null
  │  null concatenated as "null"  ← the bug
  │  returns MD5("ghost:UserDatabase:null")  ✓ matches client
  ↓
serverDigest.equals(clientDigest)  →  true
  ↓
getPrincipal("ghost")
  │  user "ghost" not in UserDatabase  →  returns null  ✗
  ↓
authenticate() returns null  →  401 Unauthorized

Conclusion

CVE-2026-43512 is a genuine bug. The null-to-"null" implicit conversion is an error, and its presence in a security-critical code path deserves a fix.

However, exploitability requires that both authentication phases succeed. In the most common deployment — UserDatabaseRealm backed by tomcat-users.xml — the second phase independently rejects unknown usernames. The CRITICAL rating in the advisory does not reflect this nuance.

The bug is confirmed. The bypass is not — in a standard configuration.

Patch to 9.0.118, 10.1.55, or 11.0.22. The fix is nine lines and has zero functional impact. Non-standard Realm implementations or future code changes may not provide the same secondary protection.


References

ResourceLink
Fix commit — 11.0.xapache/tomcat@6565a6c
PoC repositorycovepseng/cve-2026-43512-poc
RFC 2617datatracker.ietf.org/doc/html/rfc2617