← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
CVE-2021-30640 · CWE-116 · Disclosed 2021-07-12

A vulnerability in the JNDI Realm of Apache Tomcat

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
01 · The Real Story

This is a side door that only exists if you built your lobby around LDAP usernames

CVE-2021-30640 is an authentication weakness in Apache Tomcat's JNDIRealm, the LDAP-backed realm used for container-managed authentication. In affected releases, the username handling in directory lookups was not safely escaped, which can let an attacker authenticate using variations of a valid username and can also weaken LockOutRealm protections. Upstream affected ranges are Tomcat 7.0.0 to <7.0.109, 8.5.0 to <8.5.66, 9.0.0 to <9.0.46, and 10.0.0-M1 to <10.0.6; distro packages may be fixed by backport on older package versions.

The vendor's MEDIUM 6.5 is technically fair in a lab, but it overstates fleet-wide urgency for most enterprises. This is not broad unauthenticated RCE and it is not relevant to every Tomcat server; it only bites deployments that explicitly use org.apache.catalina.realm.JNDIRealm for app authentication, and the payoff is app-level auth bypass rather than host takeover.

"Real bug, narrow exposure: only matters where Tomcat is using JNDIRealm for LDAP-backed auth."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find a Tomcat app that actually uses JNDIRealm with curl or Burp

The attacker first needs a reachable application path protected by Tomcat container-managed auth rather than app-native auth, SSO middleware, or a reverse proxy identity layer. Public HTTP exposure alone is not enough; the target must ultimately hand credential validation to Tomcat's JNDIRealm against LDAP via JNDI.
Conditions required:
  • A Tomcat-hosted application is reachable from the attacker's network position
  • Authentication is enforced by Tomcat container-managed security
  • The Tomcat instance is configured with org.apache.catalina.realm.JNDIRealm
Where this breaks in practice:
  • Many enterprises use SSO, app-native auth, or non-JNDI realms instead
  • JNDIRealm usage is a configuration choice and is not universal across Tomcat estates
  • External fingerprinting usually cannot prove JNDIRealm is in use
Detection/coverage: Network scanners can find Tomcat, but most vuln scanners cannot confirm exploitability unless they inspect server.xml or package state locally.
STEP 02

Obtain or guess a valid username with ffuf or Burp Intruder

The bug does not mint arbitrary identities out of thin air; the attacker generally needs a valid naming target in the backing directory and benefits from understanding local username conventions. In practice this means enumeration from exposed app behavior, leaked usernames, email patterns, or prior directory knowledge.
Conditions required:
  • Attacker can identify at least one valid username or naming pattern
  • The realm is using a search-based lookup path affected by the escaping flaw
Where this breaks in practice:
  • Unknown usernames and opaque login responses slow exploitation materially
  • Some deployments bind directly by DN pattern rather than relying on the vulnerable search flow
  • MFA or upstream SSO can remove Tomcat from the auth path entirely
Detection/coverage: Watch for repeated auth attempts with malformed or variant usernames; web logs and LDAP auth telemetry may show unusual username strings.
STEP 03

Abuse username variation against LDAP lookup using curl or a custom script

The vulnerable code built the directory search filter from the supplied username without proper filter escaping; the upstream fix changed JNDIRealm to escape special characters before search. That opens the door to authenticating as a variation of a real user and, in some cases, bypassing parts of LockOutRealm by spreading attempts across alternate names.
Conditions required:
  • Target version falls in the vulnerable range
  • The realm's username handling and directory behavior permit an alternate form to resolve usefully
Where this breaks in practice:
  • Attack complexity is genuinely high because success depends on directory schema, username normalization, and realm configuration
  • Not every LDAP backend or naming convention will yield a workable variant
  • The bug gives auth bypass, not code execution
Detection/coverage: If you collect Tomcat access logs plus LDAP bind/search logs, correlate successful auths where the presented username differs from the canonical account name.
STEP 04

Use the gained app session with the browser or curl

Successful exploitation lands the attacker inside the protected application as whatever role mapping the resolved account receives. The blast radius is therefore constrained to that app's authorization model and any downstream trust the app has, not the underlying host by default.
Conditions required:
  • The targeted user or role has useful access inside the application
Where this breaks in practice:
  • Least-privilege app roles can sharply limit impact
  • No direct host compromise path is inherent in this CVE
  • Some apps immediately enforce additional in-app authorization checks
Detection/coverage: App audit logs are the best source: look for impossible-travel style anomalies, first-time use of dormant accounts, or successful logins with non-canonical username forms.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo CISA KEV entry and I found no primary-source evidence of active exploitation campaigns for this CVE as of 2026-06-05.
Proof-of-concept availabilityNo authoritative vendor or researcher PoC was located in reviewed primary sources. The public fix commit clearly exposes root cause, so a capable operator can reproduce it, but this is not a commodity one-click RCE.
EPSS0.00123 from the user-supplied intel — extremely low predicted near-term exploitation probability.
KEV statusNot listed in CISA's Known Exploited Vulnerabilities catalog as of 2026-06-05.
CVSS vector readoutCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:H/A:N maps to network-reachable but high-complexity exploitation, with integrity impact at the application trust boundary rather than host-level compromise.
Affected versionsUpstream affected: 7.0.0 to <7.0.109, 8.5.0 to <8.5.66, 9.0.0 to <9.0.46, 10.0.0-M1 to <10.0.6.
Fixed versionsUpstream fixes landed in 7.0.109, 8.5.66, 9.0.46, and 10.0.6. Distro backports exist, including Debian tomcat9 9.0.43-2~deb11u1 and Ubuntu tomcat9 9.0.31-1ubuntu0.2 / 9.0.16-3ubuntu0.18.04.2.
Exposure populationTomcat itself is common on the internet, but this CVE is not externally fingerprintable at scale because exploitability depends on a specific internal realm choice in server.xml and whether apps rely on Tomcat-managed auth.
Disclosure datePublished 2021-07-12.
Research / reportingThe actionable technical breadcrumb is Apache's fix commit titled *Expand tests and fix escaping issue when searching for users by filter*, which points straight at the vulnerable JNDIRealm search flow.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.8/10)

The decisive factor is exposure narrowing: this only matters on Tomcat instances that explicitly use JNDIRealm for container-managed LDAP authentication. Even where present, the result is application authentication bypass with high practical complexity, not broad internet-scale server compromise.

HIGH Low fleet-wide exposure outside explicitly configured `JNDIRealm` deployments
MEDIUM Real-world exploitability against a given LDAP schema and username convention

Why this verdict

  • Baseline down from 6.5: vendor scoring assumes any affected Tomcat is equally interesting, but this CVE only activates when JNDIRealm is actually configured in server.xml and used for auth.
  • Another step down for attacker prerequisites: exploitation needs a reachable Tomcat-managed login path plus knowledge or discovery of valid username forms. That is materially different from sprayable unauthenticated pre-auth bugs.
  • Another step down for blast radius: the payoff is app access and LockOutRealm weakening, not code execution or default host takeover. In most enterprises that keeps impact local to a subset of applications and roles.

Why not higher?

There is no strong evidence of mass exploitation, no KEV listing, and the attack surface is much narrower than 'Apache Tomcat on the internet.' The specific realm requirement plus high-complexity username/LDAP behavior makes this a poor candidate for emergency patch queues across a 10,000-host estate.

Why not lower?

It is still a genuine authentication weakness, and if you do run internet-reachable applications behind JNDIRealm, the attacker may gain unauthorized access without already holding credentials. That makes it more than paperwork on the subset of systems that actually use this feature.

05 · Compensating Control

What to do — in priority order.

  1. Identify JNDIRealm usage — Search conf/server.xml and any templated Tomcat configs for org.apache.catalina.realm.JNDIRealm first; this is the gating condition that decides whether the CVE is real for that host. For a LOW verdict there is no mitigation SLA; treat this as backlog hygiene and complete discovery in the next routine configuration review.
  2. Move auth upstream where possible — If practical, terminate user auth in SSO, reverse proxy, or an identity-aware front end instead of Tomcat container-managed LDAP auth. That removes this code path entirely; for LOW, schedule under normal change control rather than emergency action.
  3. Tighten login monitoring — Alert on repeated login attempts using unusual username variants and correlate Tomcat access logs with LDAP search/bind logs. This will not prevent exploitation, but it gives you the only realistic detection surface while the issue sits in backlog hygiene.
  4. Reduce exposed auth surfaces — Where a Tomcat app using JNDIRealm does not need broad internet reachability, place it behind VPN, ZTNA, or network ACLs. That converts an unauthenticated remote path into an internal-only path; for LOW, do it in the next routine access-hardening cycle.
What doesn't work
  • A generic WAF rule is weak here because the abuse lives in username handling and LDAP lookup semantics, not in a stable exploit payload pattern.
  • LockOutRealm by itself does not solve the problem; the CVE explicitly includes bypassing some of the protection it provides.
  • Pure Tomcat banner scanning does not tell you exposure because version alone is insufficient; you must know whether JNDIRealm is configured and in use.
06 · Verification

Crowdsourced verification payload.

Run this on the target Tomcat host or inside the container image filesystem where CATALINA_BASE is available. Invoke it as python3 check_cve_2021_30640.py /opt/tomcat (or your CATALINA_BASE), using an account that can read conf/server.xml and the Tomcat install files; root/admin is usually not required.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# check_cve_2021_30640.py
# Determine whether this Tomcat instance is exposed to CVE-2021-30640.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN

import os
import re
import sys
import zipfile
import xml.etree.ElementTree as ET

AFFECTED = [
    ((7, 0, 0), (7, 0, 109)),
    ((8, 5, 0), (8, 5, 66)),
    ((9, 0, 0), (9, 0, 46)),
    ((10, 0, 0), (10, 0, 6)),
]


def normalize_version(s):
    if not s:
        return None
    s = s.strip()
    s = s.replace('Tomcat/', '')
    s = s.replace('Apache Tomcat/', '')
    s = s.replace('Apache Tomcat', '')
    s = s.strip()
    # Handle milestone / M notation by stripping non-numeric separators after capturing digits.
    nums = re.findall(r'\d+', s)
    if len(nums) < 2:
        return None
    parts = [int(x) for x in nums[:3]]
    while len(parts) < 3:
        parts.append(0)
    return tuple(parts)


def version_in_affected(v):
    for low, high in AFFECTED:
        if v >= low and v < high:
            return True
    return False


def find_version(base):
    candidates = [
        os.path.join(base, 'RELEASE-NOTES'),
        os.path.join(base, 'RUNNING.txt'),
        os.path.join(base, 'lib', 'catalina.jar'),
        os.path.join(base, 'bin', 'version.sh'),
        os.path.join(base, 'bin', 'version.bat'),
    ]

    # Text files first
    for path in candidates[:2]:
        if os.path.isfile(path):
            try:
                with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                    data = f.read()
                m = re.search(r'Apache Tomcat(?:/|\s+)([0-9][0-9A-Za-z.\-M]*)', data)
                if m:
                    return m.group(1)
            except Exception:
                pass

    # catalina.jar manifest
    jar = candidates[2]
    if os.path.isfile(jar):
        try:
            with zipfile.ZipFile(jar, 'r') as zf:
                with zf.open('META-INF/MANIFEST.MF') as mf:
                    data = mf.read().decode('utf-8', errors='ignore')
                for line in data.splitlines():
                    if line.startswith('Implementation-Version:'):
                        return line.split(':', 1)[1].strip()
        except Exception:
            pass

    # version scripts as a last resort
    for path in candidates[3:]:
        if os.path.isfile(path):
            try:
                with open(path, 'r', encoding='utf-8', errors='ignore') as f:
                    data = f.read()
                m = re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', data)
                if m:
                    return m.group(1)
            except Exception:
                pass

    return None


def has_jndirealm(server_xml):
    try:
        tree = ET.parse(server_xml)
        root = tree.getroot()
        for elem in root.iter():
            if elem.tag.endswith('Realm'):
                cn = elem.attrib.get('className', '')
                if cn == 'org.apache.catalina.realm.JNDIRealm':
                    return True
        return False
    except Exception:
        return None


def main():
    if len(sys.argv) != 2:
        print('UNKNOWN - usage: python3 check_cve_2021_30640.py <CATALINA_BASE>')
        sys.exit(2)

    base = sys.argv[1]
    server_xml = os.path.join(base, 'conf', 'server.xml')

    if not os.path.exists(base):
        print(f'UNKNOWN - path does not exist: {base}')
        sys.exit(2)

    if not os.path.isfile(server_xml):
        print(f'UNKNOWN - cannot find {server_xml}')
        sys.exit(2)

    jndi = has_jndirealm(server_xml)
    if jndi is None:
        print(f'UNKNOWN - failed to parse {server_xml}')
        sys.exit(2)

    if not jndi:
        print('PATCHED - JNDIRealm not configured on this Tomcat instance; CVE-2021-30640 not reachable here')
        sys.exit(0)

    raw_ver = find_version(base)
    norm_ver = normalize_version(raw_ver) if raw_ver else None

    if norm_ver is None:
        print('UNKNOWN - JNDIRealm is configured but Tomcat version could not be determined')
        sys.exit(2)

    if version_in_affected(norm_ver):
        print(f'VULNERABLE - JNDIRealm configured and Tomcat version appears affected ({raw_ver})')
        sys.exit(1)
    else:
        print(f'PATCHED - JNDIRealm configured but Tomcat version appears fixed/not affected ({raw_ver})')
        sys.exit(0)


if __name__ == '__main__':
    main()
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning, do not mass-escalate every Tomcat host. First split the estate into JNDIRealm users versus everyone else; if a system does not use org.apache.catalina.realm.JNDIRealm, document it and move on. For this LOW verdict there is no noisgate mitigation SLA and no noisgate remediation SLA beyond backlog hygiene: confirm exposure in your next routine config-review cycle, prioritize any internet-facing JNDIRealm apps into the next normal Tomcat maintenance window, and fold the actual upgrade/backport into standard patch work rather than an emergency change.

Sources

  1. NVD CVE-2021-30640
  2. Ubuntu CVE page
  3. Debian security tracker
  4. Apache Tomcat fix commit
  5. Tomcat 9 Realm configuration reference
  6. Tomcat 10 Realm how-to
  7. CISA Known Exploited Vulnerabilities catalog
  8. OSV record for CVE-2021-30640
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.