Bug 2536897 (CVE-2026-93493) - CVE-2026-93493 io.netty/netty-handler-ssl-ocsp: Netty: OCSP Validation Silently Skipped When a Response Omits the Optional nextUpdate Field
Summary: CVE-2026-93493 io.netty/netty-handler-ssl-ocsp: Netty: OCSP Validation Silent...
Keywords:
Status: NEW
Alias: CVE-2026-93493
Product: Security Response
Classification: Other
Component: vulnerability
Version: unspecified
Hardware: All
OS: Linux
medium
medium
Target Milestone: ---
Assignee: Product Security
QA Contact:
URL:
Whiteboard:
Depends On:
Blocks:
TreeView+ depends on / blocked
 
Reported: 2026-09-18 07:22 UTC by OSIDB Bzimport
Modified: 2026-09-18 11:08 UTC (History)
9 users (show)

Fixed In Version:
Clone Of:
Environment:
Last Closed:
Embargoed:


Attachments (Terms of Use)

Description OSIDB Bzimport 2026-09-18 07:22:39 UTC
OCSP Validation Silently Skipped When a Response Omits the Optional nextUpdate Field

A public GitHub Security Advisory (GHSA-jj3c-mwvr-9g52) describes the following issue:

## Summary

`OcspServerCertificateValidator` bounds the age of an OCSP response only when that response carries a `nextUpdate` field. `nextUpdate` is optional in RFC 6960. When a responder omits it, the age check passes for a response of any age, the certificate status is computed, and construction of the result object then throws a `NullPointerException` because `OcspResponse` requires a non-null `nextUpdate`.

That throwable is raised inside a promise listener. It never reaches `exceptionCaught`, so nothing closes the connection, while the listener's `finally` block still removes the validator from the pipeline. The application therefore receives **no `OcspValidationEvent` and no exception at all** - revocation status is never determined, and nothing indicates that validation did not happen.

The trigger is one optional field in a response supplied by a remote party.

## Details

The age check, in `userEventTriggered`:

```java
Date current = new Date();
Date thisUpdate = response.getThisUpdate();
Date nextUpdate = response.getNextUpdate();
if (thisUpdate == null || !current.after(thisUpdate) ||
        (nextUpdate != null && !current.before(nextUpdate))) {     // upper bound is conditional
    ctx.fireExceptionCaught(new IllegalStateException("OCSP Response is out-of-date"));
    return;
}
```

With `nextUpdate` absent the third clause is skipped entirely, so a response whose `thisUpdate` is arbitrarily far in the past is treated as current. Execution continues to:

```java
ctx.fireUserEventTriggered(new OcspValidationEvent(
        new OcspResponse(status, response.getThisUpdate(), response.getNextUpdate())));
```

and `OcspResponse`'s constructor rejects the null:

```java
public OcspResponse(Status status, Date thisUpdate, Date nextUpdate) {
    this.status = checkNotNull(status, "Status");
    this.thisUpdate = checkNotNull(thisUpdate, "ThisUpdate");
    this.nextUpdate = checkNotNull(nextUpdate, "NextUpdate");     // throws
}
```

Two consequences follow from where that throw happens. It is inside a `GenericFutureListener`, so it is reported to netty's promise machinery rather than travelling down the pipeline - `exceptionCaught` is not invoked and the connection stays open. And the enclosing `finally` still runs, forwarding the original `SslHandshakeCompletionEvent` and calling `ctx.pipeline().remove(this)`, so the validator takes itself out of the pipeline having produced no verdict.

The `closeAndThrowIfNotValid` protection, which defaults to `true`, sits after the point where the exception is thrown and never executes.

## Proof of concept

`PocOcspReplay.java` builds a throwaway CA and a leaf whose Authority Information Access extension points at a local responder, runs a real TLS handshake between two netty channels with `new OcspServerCertificateValidator()` on the client, and serves three OCSP responses that differ **only** in their two date fields. Build and run with the attached `pom.xml`:

```
docker run --rm -e MAVEN_OPTS="-Djava.security.egd=file:/dev/./urandom" \
    -v "$PWD:/work" -w /work maven:3-eclipse-temurin-21 mvn -B compile exec:java
```

Observed output:

```
CONTROL-FRESH   thisUpdate=-1h   nextUpdate=+24h
   validator outcome     : OcspValidationEvent status=VALID
   app got a verdict     : yes (OcspValidationEvent)

CONTROL-EXPIRED thisUpdate=-400d nextUpdate=-399d
   validator outcome     : IllegalStateException: OCSP Response is out-of-date
   app got a verdict     : yes (exception)

ATTACK          thisUpdate=-400d nextUpdate=ABSENT
   validator outcome     : NOTHING - no OcspValidationEvent and no exception
   app got a verdict     : NO - silent
   connection left open  : true
   validator still in pipeline : false
```

The two controls carry the argument. `CONTROL-FRESH` shows the whole path works end to end and yields a `VALID` verdict. `CONTROL-EXPIRED` shows that a 400-day-old response **is** correctly recognised as stale and produces a clean, observable rejection. The attack case uses the same 400-day-old response with `nextUpdate` removed, and the application is told nothing at all.

## Impact

An application using this validator cannot distinguish "the certificate was checked and is good" from "the check never completed", because the successful case and the silent-failure case both leave it with no exception and the validator gone from the pipeline. The only difference is the presence of an `OcspValidationEvent`, and an application that treats the absence of an event as "nothing to do" proceeds with an unvalidated certificate.

Reaching this requires a response that omits `nextUpdate`. That is available to a hostile or misconfigured responder, and to an attacker able to answer the OCSP fetch - which travels over plaintext HTTP to the URL in the certificate's AIA extension, with `OcspHttpHandler` checking only the HTTP status and `Content-Type`. Note also that the default constructor disables nonce validation (`new OcspServerCertificateValidator()` delegates to `this(false)`), so a captured genuine response can be replayed; stripping `nextUpdate` from it is not required to be signed differently, because omitting the field is a matter of which response the attacker chooses to serve.

The proof of concept also records whether the application wrote data. It did so in all three cases, including both controls, because `SslHandshakeCompletionEvent` necessarily precedes the asynchronous OCSP query. That is not a differential result and we are not presenting it as one - but it does establish which signal matters. Since the handshake event always arrives first and always reports success, the `OcspValidationEvent` (or the exception) is the **only** signal by which an application can learn the revocation outcome. That is exactly the signal this defect suppresses.

On severity, the closest yardstick is this project's own. CVE-2026-56821, "Out-of-date OCSP Responses Accepted b

[truncated]

Affected:
- maven:io.netty:netty-handler-ssl-ocsp affected >= 4.2.0.Final, <=4.2.17.Final; fixed unknown
- maven:io.netty:netty-handler-ssl-ocsp affected <= 4.1.137.Final; fixed unknown

Fixed versions: see advisory

Advisory: https://github.com/netty/netty/security/advisories/GHSA-jj3c-mwvr-9g52


Note You need to log in before you can comment on or make changes to this bug.