GHSA-g7hg-vrcf-mvmr
HIGHGHSA-g7hg-vrcf-mvmr is a high-severity (CVSS 7.4) CWE-299 vulnerability in io.netty:netty-handler-ssl-ocsp. O3 Security confirms whether GHSA-g7hg-vrcf-mvmr is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Netty: Out-of-date OCSP Responses Accepted by OcspServerCertificateValidator
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-g7hg-vrcf-mvmr.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-g7hg-vrcf-mvmr plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 369,023 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
io.netty:netty-handler-ssl-ocsp☕io.netty:netty-handler-ssl-ocspReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Maven packages — download data is not available via public APIs for these ecosystems.
Description
Summary
OcspServerCertificateValidator flags an out-of-date OCSP response but does not stop processing it, so an expired GOOD response is still reported as VALID, letting an on-path attacker replay a stale GOOD response to bypass revocation of a since-revoked certificate.
Details
In io.netty.handler.ssl.ocsp.OcspServerCertificateValidator#userEventTriggered the freshness check has no return, so execution falls through and a VALID OcspValidationEvent is still fired:
if (!(current.after(response.getThisUpdate()) &&
current.before(response.getNextUpdate()))) {
ctx.fireExceptionCaught(new IllegalStateException("OCSP Response is out-of-date"));
}
Nonce validation is optional and off by default, so freshness is the only replay defense — and it is not enforced. Additionally getNextUpdate() may be null, making current.before(null) throw NullPointerException.
https://datatracker.ietf.org/doc/html/rfc6960#section-3.2
5. The time at which the status being indicated is known to be
correct (thisUpdate) is sufficiently recent;
6. When available, the time at or before which newer information will
be available about the status of the certificate (nextUpdate) is
greater than the current time.
PoC
Add the test below to io.netty.handler.ssl.ocsp.OcspServerCertificateValidatorTest
@Test
void staleOcspResponseIsRejected() throws Exception {
X509Bundle caRoot = new CertificateBuilder()
.algorithm(CertificateBuilder.Algorithm.rsa2048)
.subject("CN=TrustedRootCA")
.setIsCertificateAuthority(true)
.buildSelfSigned();
GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, "http://localhost/");
AuthorityInformationAccess aia = new AuthorityInformationAccess(
new AccessDescription(AccessDescription.id_ad_ocsp, ocspName));
X509Bundle targetCert = new CertificateBuilder()
.algorithm(CertificateBuilder.Algorithm.rsa2048)
.subject("CN=TargetServer")
.addExtensionOctetString("1.3.6.1.5.5.7.1.1", false, aia.getEncoded())
.buildIssuedBy(caRoot);
Date past = new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7));
CertificateID certId = new CertificateID(
new JcaDigestCalculatorProviderBuilder().build().get(CertificateID.HASH_SHA1),
new JcaX509CertificateHolder(caRoot.getCertificate()),
targetCert.getCertificate().getSerialNumber());
BasicOCSPRespBuilder respBuilder = new BasicOCSPRespBuilder(
new RespID(new JcaX509CertificateHolder(caRoot.getCertificate()).getSubject()));
respBuilder.addResponse(certId, CertificateStatus.GOOD, past, past);
BasicOCSPResp expiredBasicResp = respBuilder.build(
new JcaContentSignerBuilder("SHA256withRSA").build(caRoot.getKeyPair().getPrivate()),
new X509CertificateHolder[0],
past);
final byte[] responseEncoded = new OCSPRespBuilder()
.build(OCSPRespBuilder.SUCCESSFUL, expiredBasicResp).getEncoded();
IoTransport defaultTransport = createDefaultTransport();
IoTransport mockTransport = IoTransport.create(defaultTransport.eventLoop(), () -> {
NioSocketChannel channel = new NioSocketChannel();
channel.pipeline().addFirst(new ChannelOutboundHandlerAdapter() {
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,
SocketAddress localAddress, ChannelPromise promise) {
promise.setSuccess();
ctx.executor().execute(() -> {
ctx.pipeline().fireChannelActive();
DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
Unpooled.wrappedBuffer(responseEncoded));
httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/ocsp-response");
httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,
httpResponse.content().readableBytes());
ctx.pipeline().fireChannelRead(httpResponse);
});
}
});
return channel;
}, defaultTransport.datagramChannel());
SslContext serverSslCtx = SslContextBuilder
.forServer(targetCert.getKeyPair().getPrivate(),
targetCert.getCertificate(), caRoot.getCertificate())
.build();
Channel serverChannel = new ServerBootstrap()
.group(defaultTransport.eventLoop())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(serverSslCtx.newHandler(ch.alloc()));
}
})
.bind(0).sync().channel();
int serverPort = ((InetSocketAddress) serverChannel.localAddress()).getPort();
AtomicBoolean validEventFired = new AtomicBoolean();
AtomicReference<Throwable> caughtException = new AtomicReference<>();
CountDownLatch latch = new CountDownLatch(1);
DnsNameResolver resolver = OcspServerCertificateValidator.createDefaultResolver(mockTransport);
SslContext clientSslCtx = SslContextBuilder.forClient()
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.build();
new Bootstrap()
.group(defaultTransport.eventLoop())
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(clientSslCtx.newHandler(ch.alloc(), "127.0.0.1", serverPort));
ch.pipeline().addLast(
new OcspServerCertificateValidator(true, false, mockTransport, resolver));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof OcspValidationEvent &&
((OcspValidationEvent) evt).response().status() ==
OcspResponse.Status.VALID) {
validEventFired.set(true);
}
ctx.fireUserEventTriggered(evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
caughtException.compareAndSet(null, cause);
ctx.channel().close();
latch.countDown();
}
});
}
})
.connect("127.0.0.1", serverPort).sync();
assertTrue(latch.await(5, TimeUnit.SECONDS));
assertFalse(validEventFired.get(),
"OcspValidationEvent(VALID) must not be emitted for a stale OCSP response");
assertNotNull(caughtException.get());
assertInstanceOf(IllegalStateException.class, caughtException.get());
serverChannel.close().sync();
resolver.close();
}
Impact
Certificate revocation bypass via replay of an expired OCSP response. Any application using OcspServerCertificateValidator is affected; a revoked certificate can be accepted.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| ☕Maven | io.netty:netty-handler-ssl-ocsp | ≥ 4.2.0.Final&&< 4.2.16.Final | 4.2.16.Final |
| ☕Maven | io.netty:netty-handler-ssl-ocsp | all versions | 4.1.136.Final |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for io.netty:netty-handler-ssl-ocsp. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update io.netty:netty-handler-ssl-ocsp to 4.2.16.Final or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-g7hg-vrcf-mvmr is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-g7hg-vrcf-mvmr is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-g7hg-vrcf-mvmr. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-g7hg-vrcf-mvmr in your dependencies?
O3 detects GHSA-g7hg-vrcf-mvmr across Maven dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.