{"id":"CVE-2026-56821","aliases":["GHSA-g7hg-vrcf-mvmr"],"url":"https://o3.security/vulnerability/CVE-2026-56821","summary":"Netty: Out-of-date OCSP Responses Accepted by OcspServerCertificateValidator","details":"### 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.","published":"2026-07-28T23:07:13.616Z","modified":"2026-10-21T02:30:44.516592593Z","cvss":{"score":7.4,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N"},"epss":{"score":0.00164,"percentile":0.06009,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Maven","name":"io.netty:netty-handler-ssl-ocsp","fixedVersion":"4.2.16.Final"},{"ecosystem":"Maven","name":"io.netty:netty-handler-ssl-ocsp","fixedVersion":"4.1.136.Final"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/56xxx/CVE-2026-56821.json"},{"type":"ADVISORY","url":"https://github.com/netty/netty/security/advisories/GHSA-g7hg-vrcf-mvmr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-56821"},{"type":"PACKAGE","url":"https://github.com/netty/netty"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.1.136.Final"},{"type":"WEB","url":"https://github.com/netty/netty/releases/tag/netty-4.2.16.Final"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T02:30:44.516592593Z"}}