← Back to Feed CACHED · 2026-05-17 09:42:19 · cache_key CVE-2025-29912
tenable:158252 · CWE-502 · Disclosed 2022-01-05

H2 Database JNDI Lookup RCE

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

This is a loaded nail gun left in the dev lab, not a landmine buried under every server

CVE-2021-42392 is a JNDI-based remote code execution flaw in H2 Console affecting H2 versions 1.1.100 through 2.0.204, fixed in 2.0.206. An attacker can abuse the console login form by supplying a JNDI driver such as javax.naming.InitialContext and an attacker-controlled url like ldap://..., which reaches org.h2.util.JdbcUtils.getConnection() before credentials are meaningfully validated. The most dangerous path is the web console; there are also SQL-based paths, but those require ADMIN privileges and are basically post-compromise.

The vendor/NVD CRITICAL 9.8 rating is technically understandable but operationally inflated for most enterprises. The big friction point is that H2 Console is localhost-only by default and many H2 deployments are embedded or dev/test-only, so the reachable population is far smaller than the CVSS vector implies; however, any instance that is reachable from user subnets or the internet is still a serious unauthenticated RCE with public weaponization patterns and easy scanability.

"Pre-auth RCE if the H2 console is exposed, but default-safe config and narrow exposure keep this out of CRITICAL."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Find an exposed H2 console

Attackers first identify an H2 web console endpoint, typically the vanilla root page on port 8082 or a framework-mounted path such as /h2-console. JFrog explicitly recommends nmap title-based scanning and notes public search engines can locate WAN-facing consoles, which means discovery is cheap once admins expose the service.
Conditions required:
  • H2 Console is running
  • The console is reachable from the attacker network position
  • Remote access was enabled directly or via a third-party framework/servlet deployment
Where this breaks in practice:
  • Vanilla H2 listens on localhost by default
  • Many H2 deployments do not run the console at all
  • Reverse proxies, firewalls, or private-only dev segments often block reachability
Detection/coverage: Strong external attack-surface management coverage if you fingerprint HTTP titles, ports, or /h2-console; authenticated SCA/SBOM tools will find the library but not whether the console is actually exposed.
STEP 02

Abuse the login form with JNDI parameters

Using a browser, curl, or a simple exploit script, the attacker submits a crafted driver and JDBC url to the H2 login form. Per JFrog, the console passes these into JdbcUtils.getConnection() and performs the JNDI lookup before the supplied username and password matter, making the console path effectively pre-auth.
Conditions required:
  • Access to the H2 login page
  • Vulnerable H2 version below 2.0.206
Where this breaks in practice:
  • If the console servlet is wrapped with a proper security constraint, unauthenticated access dies here
  • WAFs may block obvious ldap:// or rmi:// payloads, though many internal admin apps sit behind no WAF at all
Detection/coverage: Web logs and reverse-proxy logs can catch suspicious driver=javax.naming.InitialContext and ldap:// / rmi:// strings; most vulnerability scanners can verify the exposed console, but exploit-attempt telemetry depends on request logging.
STEP 03

Serve the JNDI payload

The attacker stands up an LDAP or RMI server using common tooling such as marshalsec-style JNDI infrastructure and returns a malicious reference or serialized gadget chain. Newer Java versions enable trustURLCodebase protections by default, but JFrog notes that serialized gadget delivery can still work if useful gadget classes already exist on the target classpath.
Conditions required:
  • Target can reach attacker-controlled LDAP/RMI infrastructure or an internal redirector
  • Either remote class loading is allowed or a local gadget chain exists in the target classpath
Where this breaks in practice:
  • Egress filtering often blocks outbound LDAP/RMI from servers
  • Modern JRE defaults reduce naive remote class loading success
  • Reliable gadget availability varies by application stack
Detection/coverage: EDR, egress firewalls, DNS/LDAP monitoring, and JVM crash/exception telemetry can surface this step; network IDS may detect outbound LDAP/RMI from app servers, which is usually rare enough to alert on.
STEP 04

Execute code in the H2/web-server process

If the lookup succeeds, attacker-controlled code executes in the context of the H2 Server process or the web server hosting the H2 Console servlet. That usually means full compromise of that application node, access to app secrets and local data, and a strong beachhead for lateral movement.
Conditions required:
  • Successful JNDI/gadget execution
  • Process permissions sufficient for follow-on actions
Where this breaks in practice:
  • Container isolation, non-root runtime, and restricted service accounts can limit blast radius
  • Ephemeral dev containers may reduce persistence value
Detection/coverage: EDR should catch child processes, suspicious JVM network activity, shell drops, or abnormal classloading; if no EDR is present on build/dev nodes, this step is often missed.
03 · Intelligence Metadata

The supporting signals.

In-the-wild statusNo authoritative KEV listing found in the CISA sources reviewed. JFrog's April 30, 2026 update says no widespread active exploitation has been reported; DIVD later wrote it was 'reported as exploited in the wild,' so treat exploitation evidence as mixed and low-confidence, not clean KEV-grade proof.
Proof-of-concept availabilityPublicly weaponizable. JFrog published the attack mechanics; DIVD states PoCs are available on the internet; GitHub and Packet Storm references are widely cited by downstream advisories.
EPSS0.91037 / 99.6th percentile per Docker Scout. That is unusually high, but here I would still let exposure friction outrank EPSS for enterprise scheduling.
KEV statusNot observed in CISA KEV based on the catalog/search sources reviewed. That matters: if this were broadly and repeatedly exploited at scale, I would expect stronger CISA signal by now.
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H (9.8 CRITICAL). The technical vector assumes the vulnerable surface is reachable; real deployments often fail that assumption because the console is not remotely exposed by default.
Affected versionsH2 Console 1.1.100-2.0.204 per the GitHub advisory; NVD CPE enrichment broadly tracks H2 up to 2.0.204. The highest-risk subset is vulnerable H2 plus an exposed console or servlet path.
Fixed versions2.0.206 fixes CVE-2021-42392 in upstream H2. Debian also backported protections while noting the console is a developer tool: 1.4.197-4+deb10u1 for buster and 1.4.197-4+deb11u1 for bullseye.
Scanning / exposure realityJFrog says WAN-facing consoles are findable with public search tools and provides nmap guidance; DIVD started internet scanning on 2022-10-07 and sent notifications, which is a good signal that exposed instances existed in meaningful numbers even if the global population was still limited.
Disclosure timelineGitHub advisory published 2022-01-05; JFrog technical write-up published 2022-01-06; NVD published the CVE on 2022-01-10.
Researcher / originPrivately reported by JFrog Security; the JFrog blog credits Andrey Polkovnychenko and Shachar Menashe, and later added credit to @pyn3rd for similar prior findings.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to HIGH (8.3/10)

The decisive factor is reachability friction: the unauthenticated RCE is real, but the H2 console is localhost-only by default and many H2 installs never expose the console at all. That sharply narrows the exploitable population compared with a true internet-by-default component, while any exposed instance still represents a high-consequence compromise path.

HIGH Technical impact of exposed-console exploitation
MEDIUM Enterprise prevalence of remotely reachable H2 consoles
LOW Claims of broad in-the-wild exploitation

Why this verdict

  • Downgrade for default-safe posture: vanilla H2 Console does not accept remote connections by default, so the vendor 9.8 assumes a reachable surface that many deployments simply do not expose.
  • Downgrade for attacker position requirements in practice: the clean unauthenticated path usually requires user-subnet or internet reachability to an admin/dev console. If it is only reachable internally, the bug is already post-initial-access and loses urgency relative to perimeter RCEs.
  • Downgrade for exposure fraction: H2 is popular as an embedded library, but the exploitable path is specifically the console or another context that forwards attacker-controlled JNDI parameters. That is a much smaller population than 'all H2 on the classpath.'
  • Keep it HIGH because exploitation is straightforward once reachable: no auth is needed on the console path, public technical detail exists, scanning is easy, and successful exploitation gives code execution in the application process.
  • Keep it HIGH because developer tooling is a soft target: JFrog called out ecosystems like JHipster and developer-oriented deployments where webAllowOthers or /h2-console exposure shows up more often than it should.

Why not higher?

I am not calling this CRITICAL because the most important prerequisite is not the version number, it is console exposure. A bug that needs an otherwise-nondefault admin surface to be remotely reachable is not in the same operational bucket as Exchange, vCenter, or edge-device RCEs that are commonly internet-facing by design. Mixed evidence on live exploitation also keeps this below the top bucket.

Why not lower?

I am not dropping this to MEDIUM because exposed instances are one-request pre-auth RCE with full node compromise potential. If your attack surface scan shows even a handful of reachable H2 consoles, this immediately stops being theoretical and deserves fast handling.

05 · Compensating Control

What to do — in priority order.

  1. Block remote access to H2 Console — Remove external and broad internal reachability to H2 Console endpoints and standalone ports, especially 8082 and framework paths like /h2-console. For a HIGH verdict, deploy this within 30 days; if you already know an instance is internet-exposed, do it in the first change window, not at day 29.
  2. Disable webAllowOthers — Set webAllowOthers=false or remove the flag entirely so vanilla H2 stays localhost-only. Apply within 30 days because this is the single configuration change that kills the main unauthenticated path without waiting for a full application rebuild.
  3. Require authz at the servlet layer — If the console must exist temporarily, wrap the H2 servlet behind a proper security constraint or reverse-proxy access control limited to named admins and trusted jump hosts. Deploy within 30 days as a containment measure for apps that cannot be rebuilt immediately.
  4. Restrict outbound LDAP/RMI — Block unnecessary egress from application servers to LDAP, RMI, and arbitrary high-risk destinations. This does not fully solve the bug, but within 30 days it meaningfully raises exploit friction by breaking the common JNDI callback stage.
  5. Harden the Java runtime — Ensure runtimes are on JRE/JDK lines where trustURLCodebase protections are on by default (6u211, 7u201, 8u191, 11.0.1+ per JFrog). Do this within 30 days as defense-in-depth, not as a substitute for fixing H2.
What doesn't work
  • Upgrading Java alone is not enough. JFrog notes JNDI remote classloading mitigations can be bypassed with serialized gadget delivery if suitable gadget classes are already on the classpath.
  • Relying on the H2 login page is not protection. The vulnerable lookup happens before the credentials provide meaningful security on the console path.
  • SCA/SBOM-only visibility is not enough. It will tell you com.h2database:h2 exists, but not whether the dangerous console surface is remotely reachable.
06 · Verification

Crowdsourced verification payload.

Run this on the target host or on an auditor workstation with filesystem access to the application directories. Invoke it as python3 verify_h2_cve_2021_42392.py /opt /srv /app with read access only; root/admin is not required unless your app directories are restricted. The script looks for H2 jars, extracts versions, and searches nearby config files for exposure indicators such as webAllowOthers=true or spring.h2.console.enabled=true.

noisgate-verify.py
PYTHONREAD-ONLYSAFE
#!/usr/bin/env python3
# verify_h2_cve_2021_42392.py
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN, 3=usage/error

import os
import re
import sys
from pathlib import Path

MAX_FILES = 25000
MAX_FILE_SIZE = 2 * 1024 * 1024
CONFIG_EXTS = {'.properties', '.yml', '.yaml', '.xml', '.conf', '.cfg', '.ini', '.json', '.sh', '.bat', '.cmd', '.ps1'}

VULN_MIN = (1, 1, 100)
FIXED = (2, 0, 206)

jar_hits = []
exposure_hits = []
scanned = 0

version_patterns = [
    re.compile(r'h2[-_](\d+)\.(\d+)\.(\d+)\.jar$', re.I),
    re.compile(r'/com/h2database/h2/(\d+)\.(\d+)\.(\d+)/h2-\1\.\2\.\3\.jar$', re.I),
    re.compile(r'\bversion\b\s*[:=]\s*["\']?(\d+)\.(\d+)\.(\d+)["\']?', re.I),
]

indicator_patterns = [
    re.compile(r'\bwebAllowOthers\s*=\s*true\b', re.I),
    re.compile(r'\b-webAllowOthers\b', re.I),
    re.compile(r'\bspring\.h2\.console\.enabled\s*[:=]\s*true\b', re.I),
    re.compile(r'\bspring\.h2\.console\.path\s*[:=]\s*/h2-console\b', re.I),
    re.compile(r'\b/h2-console\b', re.I),
    re.compile(r'Server\.createWebServer\([^\)]*-webAllowOthers', re.I),
]


def parse_ver_tuple(text):
    for pat in version_patterns:
        m = pat.search(text)
        if m:
            return tuple(int(x) for x in m.groups())
    return None


def ver_lt(a, b):
    return a < b


def ver_gte(a, b):
    return a >= b


def is_vuln_version(v):
    return ver_gte(v, VULN_MIN) and ver_lt(v, FIXED)


def scan_file(path):
    global scanned
    if scanned >= MAX_FILES:
        return
    scanned += 1
    try:
        st = path.stat()
        if not path.is_file() or st.st_size > MAX_FILE_SIZE:
            return
    except Exception:
        return

    low = path.name.lower()
    if low.endswith('.jar') and 'h2' in low:
        version = parse_ver_tuple(str(path))
        if version is not None:
            jar_hits.append((str(path), version))
        return

    if path.suffix.lower() not in CONFIG_EXTS and 'h2' not in low:
        return

    try:
        data = path.read_text(errors='ignore')
    except Exception:
        return

    if 'h2' in low and low.endswith('.jar'):
        version = parse_ver_tuple(data)
        if version is not None:
            jar_hits.append((str(path), version))

    for pat in indicator_patterns:
        for m in pat.finditer(data):
            line_start = data.rfind('\n', 0, m.start()) + 1
            line_end = data.find('\n', m.end())
            if line_end == -1:
                line_end = len(data)
            snippet = data[line_start:line_end].strip()
            exposure_hits.append((str(path), snippet[:220]))
            break


def walk_roots(roots):
    skip_dirs = {'.git', '.svn', 'node_modules', '__pycache__', 'proc', 'sys', 'dev', 'run', 'tmp', 'var/lib/docker/overlay2'}
    for root in roots:
        p = Path(root)
        if not p.exists():
            continue
        if p.is_file():
            scan_file(p)
            continue
        for base, dirs, files in os.walk(p, topdown=True):
            dirs[:] = [d for d in dirs if d not in skip_dirs]
            for name in files:
                if scanned >= MAX_FILES:
                    return
                scan_file(Path(base) / name)


def fmt_ver(v):
    return '.'.join(str(x) for x in v)


def main():
    if len(sys.argv) < 2:
        print('Usage: python3 verify_h2_cve_2021_42392.py <path> [<path> ...]')
        sys.exit(3)

    walk_roots(sys.argv[1:])

    vuln_jars = [(p, v) for p, v in jar_hits if is_vuln_version(v)]
    patched_jars = [(p, v) for p, v in jar_hits if not is_vuln_version(v) and ver_gte(v, FIXED)]

    if vuln_jars and exposure_hits:
        print('VULNERABLE')
        print('Reason: vulnerable H2 version(s) found with exposure indicator(s).')
        for p, v in vuln_jars[:10]:
            print(f'  H2: {p} -> {fmt_ver(v)}')
        for p, s in exposure_hits[:10]:
            print(f'  Exposure: {p} -> {s}')
        sys.exit(1)

    if vuln_jars and not exposure_hits:
        print('UNKNOWN')
        print('Reason: vulnerable H2 version(s) found, but no clear console-exposure indicator was discovered nearby.')
        for p, v in vuln_jars[:10]:
            print(f'  H2: {p} -> {fmt_ver(v)}')
        sys.exit(2)

    if patched_jars and not vuln_jars:
        print('PATCHED')
        print('Reason: only fixed H2 version(s) found (>= 2.0.206).')
        for p, v in patched_jars[:10]:
            print(f'  H2: {p} -> {fmt_ver(v)}')
        sys.exit(0)

    print('UNKNOWN')
    print('Reason: no H2 jars detected in supplied paths, or version/config could not be determined.')
    sys.exit(2)


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

If you remember one thing.

TL;DR
Monday morning, split this into two populations: reachable H2 consoles and everything else. Use attack-surface data and host checks to find any instance exposing 8082 or /h2-console; for those, apply compensating controls immediately and complete them well inside the noisgate mitigation SLA of ≤30 days, then patch to 2.0.206+ or a supported backport within the noisgate remediation SLA of ≤180 days. For vulnerable H2 libraries that are not exposing the console, this is still worth fixing, but it is not a fleet-wide fire drill—treat internet- or user-reachable consoles as the real priority bucket.

Sources

  1. Tenable plugin 158252
  2. NVD CVE-2021-42392
  3. GitHub Security Advisory GHSA-h376-j262-vhq6
  4. JFrog technical analysis
  5. Docker Scout advisory with EPSS
  6. DIVD case and internet scanning timeline
  7. H2 tutorial / console settings
  8. Debian DSA-5076 backport advisory
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.