{"id":"CVE-2026-56821","aliases":["GHSA-g7hg-vrcf-mvmr"],"title":"Netty: Out-of-date OCSP Responses Accepted by OcspServerCertificateValidator","summary":"Netty: Out-of-date OCSP Responses Accepted by OcspServerCertificateValidator","severity":"high","cvss":7.4,"cwe":["CWE-299"],"vendor":"netty","product":"io.netty:netty-handler-ssl-ocsp","ecosystem":"maven","affected":["io.netty:netty-handler-ssl-ocsp >= 4.2.0.Final, < 4.2.16.Final","io.netty:netty-handler-ssl-ocsp < 4.1.136.Final"],"patched":["io.netty:netty-handler-ssl-ocsp 4.2.16.Final","io.netty:netty-handler-ssl-ocsp 4.1.136.Final"],"published":"2026-07-22","updated":"2026-07-22","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-g7hg-vrcf-mvmr","references":[{"url":"https://github.com/netty/netty/security/advisories/GHSA-g7hg-vrcf-mvmr"},{"url":"https://github.com/netty/netty/releases/tag/netty-4.1.136.Final"},{"url":"https://github.com/netty/netty/releases/tag/netty-4.2.16.Final"},{"url":"https://github.com/advisories/GHSA-g7hg-vrcf-mvmr"}],"tags":["ghsa","maven"],"ingestedAt":"2026-07-22T22:06:57.750Z","epss":0.00219,"epssPercentile":0.11003,"slug":"CVE-2026-56821","body":"## Overview\n\n### Summary\n`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.\n\n### Details\nIn `io.netty.handler.ssl.ocsp.OcspServerCertificateValidator#userEventTriggered` the freshness check has no `return`, so execution falls through and a `VALID` `OcspValidationEvent` is still fired:\n\n```java\n                        if (!(current.after(response.getThisUpdate()) &&\n                                current.before(response.getNextUpdate()))) {\n                            ctx.fireExceptionCaught(new IllegalStateException(\"OCSP Response is out-of-date\"));\n                        }\n```\n\nNonce 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`.\n\nhttps://datatracker.ietf.org/doc/html/rfc6960#section-3.2\n\n```\n   5. The time at which the status being indicated is known to be\n      correct (thisUpdate) is sufficiently recent;\n\n   6. When available, the time at or before which newer information will\n      be available about the status of the certificate (nextUpdate) is\n      greater than the current time.\n```\n\n### PoC\n\nAdd the test below to `io.netty.handler.ssl.ocsp.OcspServerCertificateValidatorTest`\n\n```java\n    @Test\n    void staleOcspResponseIsRejected() throws Exception {\n        X509Bundle caRoot = new CertificateBuilder()\n                .algorithm(CertificateBuilder.Algorithm.rsa2048)\n                .subject(\"CN=TrustedRootCA\")\n                .setIsCertificateAuthority(true)\n                .buildSelfSigned();\n\n        GeneralName ocspName = new GeneralName(GeneralName.uniformResourceIdentifier, \"http://localhost/\");\n        AuthorityInformationAccess aia = new AuthorityInformationAccess(\n                new AccessDescription(AccessDescription.id_ad_ocsp, ocspName));\n        X509Bundle targetCert = new CertificateBuilder()\n                .algorithm(CertificateBuilder.Algorithm.rsa2048)\n                .subject(\"CN=TargetServer\")\n                .addExtensionOctetString(\"1.3.6.1.5.5.7.1.1\", false, aia.getEncoded())\n                .buildIssuedBy(caRoot);\n\n        Date past = new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7));\n        CertificateID certId = new CertificateID(\n                new JcaDigestCalculatorProviderBuilder().build().get(CertificateID.HASH_SHA1),\n                new JcaX509CertificateHolder(caRoot.getCertificate()),\n                targetCert.getCertificate().getSerialNumber());\n        BasicOCSPRespBuilder respBuilder = new BasicOCSPRespBuilder(\n                new RespID(new JcaX509CertificateHolder(caRoot.getCertificate()).getSubject()));\n        respBuilder.addResponse(certId, CertificateStatus.GOOD, past, past);\n        BasicOCSPResp expiredBasicResp = respBuilder.build(\n                new JcaContentSignerBuilder(\"SHA256withRSA\").build(caRoot.getKeyPair().getPrivate()),\n                new X509CertificateHolder[0],\n                past);\n        final byte[] responseEncoded = new OCSPRespBuilder()\n                .build(OCSPRespBuilder.SUCCESSFUL, expiredBasicResp).getEncoded();\n\n        IoTransport defaultTransport = createDefaultTransport();\n        IoTransport mockTransport = IoTransport.create(defaultTransport.eventLoop(), () -> {\n                NioSocketChannel channel = new NioSocketChannel();\n                channel.pipeline().addFirst(new ChannelOutboundHandlerAdapter() {\n                    @Override\n                    public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress,\n                                        SocketAddress localAddress, ChannelPromise promise) {\n                        promise.setSuccess();\n                        ctx.executor().execute(() -> {\n                            ctx.pipeline().fireChannelActive();\n                            DefaultFullHttpResponse httpResponse = new DefaultFullHttpResponse(\n                                    HttpVersion.HTTP_1_1, HttpResponseStatus.OK,\n                                    Unpooled.wrappedBuffer(responseEncoded));\n                            httpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, \"application/ocsp-response\");\n                            httpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH,\n                                    httpResponse.content().readableBytes());\n                            ctx.pipeline().fireChannelRead(httpResponse);\n                        });\n                    }\n                });\n                return channel;\n            }, defaultTransport.datagramChannel());\n\n            SslContext serverSslCtx = SslContextBuilder\n                    .forServer(targetCert.getKeyPair().getPrivate(),\n                            targetCert.getCertificate(), caRoot.getCertificate())\n                    .build();\n            Channel serverChannel = new ServerBootstrap()\n                    .group(defaultTransport.eventLoop())\n                    .channel(NioServerSocketChannel.class)\n                    .childHandler(new ChannelInitializer<SocketChannel>() {\n                        @Override\n                        protected void initChannel(SocketChannel ch) {\n                            ch.pipeline().addLast(serverSslCtx.newHandler(ch.alloc()));\n                        }\n                    })\n                    .bind(0).sync().channel();\n\n            int serverPort = ((InetSocketAddress) serverChannel.localAddress()).getPort();\n\n            AtomicBoolean validEventFired = new AtomicBoolean();\n            AtomicReference<Throwable> caughtException = new AtomicReference<>();\n            CountDownLatch latch = new CountDownLatch(1);\n\n            DnsNameResolver resolver = OcspServerCertificateValidator.createDefaultResolver(mockTransport);\n            SslContext clientSslCtx = SslContextBuilder.forClient()\n                    .trustManager(InsecureTrustManagerFactory.INSTANCE)\n                    .build();\n            new Bootstrap()\n                    .group(defaultTransport.eventLoop())\n                    .channel(NioSocketChannel.class)\n                    .handler(new ChannelInitializer<SocketChannel>() {\n                        @Override\n                        protected void initChannel(SocketChannel ch) {\n                            ch.pipeline().addLast(clientSslCtx.newHandler(ch.alloc(), \"127.0.0.1\", serverPort));\n                            ch.pipeline().addLast(\n                                    new OcspServerCertificateValidator(true, false, mockTransport, resolver));\n                            ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {\n                                @Override\n                                public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {\n                                    if (evt instanceof OcspValidationEvent &&\n                                            ((OcspValidationEvent) evt).response().status() ==\n                                                    OcspResponse.Status.VALID) {\n                                        validEventFired.set(true);\n                                    }\n                                    ctx.fireUserEventTriggered(evt);\n                                }\n\n                                @Override\n                                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {\n                                    caughtException.compareAndSet(null, cause);\n                                    ctx.channel().close();\n                                    latch.countDown();\n                                }\n                            });\n                        }\n                    })\n                    .connect(\"127.0.0.1\", serverPort).sync();\n\n            assertTrue(latch.await(5, TimeUnit.SECONDS));\n            assertFalse(validEventFired.get(),\n                    \"OcspValidationEvent(VALID) must not be emitted for a stale OCSP response\");\n            assertNotNull(caughtException.get());\n            assertInstanceOf(IllegalStateException.class, caughtException.get());\n\n            serverChannel.close().sync();\n            resolver.close();\n    }\n```\n### Impact\nCertificate revocation bypass via replay of an expired OCSP response. Any application using `OcspServerCertificateValidator` is affected; a revoked certificate can be accepted.\n\n## Affected packages\n\n- `io.netty:netty-handler-ssl-ocsp >= 4.2.0.Final, < 4.2.16.Final`\n- `io.netty:netty-handler-ssl-ocsp < 4.1.136.Final`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `io.netty:netty-handler-ssl-ocsp 4.2.16.Final`\n- `io.netty:netty-handler-ssl-ocsp 4.1.136.Final`","depth":"twilight","depthScore":41,"depthScoreParts":{"impact":40.7,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}