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.
4 steps from start to impact.
Find an exposed H2 console
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.- 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
- 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
/h2-console; authenticated SCA/SBOM tools will find the library but not whether the console is actually exposed.Abuse the login form with JNDI parameters
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.- Access to the H2 login page
- Vulnerable H2 version below 2.0.206
- If the console servlet is wrapped with a proper security constraint, unauthenticated access dies here
- WAFs may block obvious
ldap://orrmi://payloads, though many internal admin apps sit behind no WAF at all
driver=javax.naming.InitialContext and ldap:// / rmi:// strings; most vulnerability scanners can verify the exposed console, but exploit-attempt telemetry depends on request logging.Serve the JNDI payload
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.- 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
- 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
Execute code in the H2/web-server process
- Successful JNDI/gadget execution
- Process permissions sufficient for follow-on actions
- Container isolation, non-root runtime, and restricted service accounts can limit blast radius
- Ephemeral dev containers may reduce persistence value
The supporting signals.
| In-the-wild status | No 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 availability | Publicly 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. |
| EPSS | 0.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 status | Not 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 vector | CVSS: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 versions | H2 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 versions | 2.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 reality | JFrog 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 timeline | GitHub advisory published 2022-01-05; JFrog technical write-up published 2022-01-06; NVD published the CVE on 2022-01-10. |
| Researcher / origin | Privately reported by JFrog Security; the JFrog blog credits Andrey Polkovnychenko and Shachar Menashe, and later added credit to @pyn3rd for similar prior findings. |
noisgate verdict.
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.
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
webAllowOthersor/h2-consoleexposure 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.
What to do — in priority order.
- Block remote access to H2 Console — Remove external and broad internal reachability to H2 Console endpoints and standalone ports, especially
8082and 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. - Disable
webAllowOthers— SetwebAllowOthers=falseor 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. - 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.
- 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.
- Harden the Java runtime — Ensure runtimes are on JRE/JDK lines where
trustURLCodebaseprotections 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.
- 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:h2exists, but not whether the dangerous console surface is remotely reachable.
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.
#!/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()
If you remember one thing.
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
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.