VYPR
Moderate severityNVD Advisory· Published Feb 6, 2026· Updated Feb 9, 2026

client-certificate-auth has an Open Redirect via Host Header Injection in HTTP-to-HTTPS redirect

CVE-2026-25651

Description

client-certificate-auth is middleware for Node.js implementing client SSL certificate authentication/authorization. Versions 0.2.1 and 0.3.0 of client-certificate-auth contain an open redirect vulnerability. The middleware unconditionally redirects HTTP requests to HTTPS using the unvalidated Host header, allowing an attacker to redirect users to arbitrary domains. This vulnerability is fixed in 1.0.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An open redirect vulnerability in client-certificate-auth Node.js middleware allows attackers to redirect users to arbitrary domains via unvalidated Host header.

The vulnerability, present in versions 0.2.1 and 0.3.0 of the client-certificate-auth Node.js middleware, is an open redirect. The middleware unconditionally redirects HTTP requests to HTTPS using the unvalidated Host header, allowing an attacker to redirect users to arbitrary domains [1][2].

An attacker can exploit this by crafting an HTTP request with a manipulated Host header pointing to a malicious domain. The middleware, before performing any client certificate authentication, redirects the request to HTTPS using that Host header, effectively sending the user anywhere the attacker specifies [2]. No authentication is required to trigger the redirect.

The impact is that an attacker can redirect users to phishing sites or other malicious destinations, potentially leading to credential theft or malware installation [2].

The vulnerability is fixed in version 1.0.0, which removed the insecure redirect functionality and added options like verifyHeader and verifyValue for defense in depth [3][4]. Users should upgrade to version 1.0.0 or later to mitigate the risk [1][4].

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
client-certificate-authnpm
>= 0.2.1, < 1.0.01.0.0

Affected products

2

Patches

1
8fc995e953db

fix(security): remove redirectInsecure, add verifyHeader/verifyValue

10 files changed · +219 157
  • lib/clientCertificateAuth.cjs+1 6 modified
    @@ -29,14 +29,9 @@ async function loadModule() {
      * @returns {Function} Express middleware
      */
     function clientCertificateAuth(callback, options = {}) {
    -    const { redirectInsecure = false, includeChain = false } = options;
    +    const { includeChain = false } = options;
     
         return function middleware(req, res, next) {
    -        // Optional HTTPS redirect (opt-in only due to security concerns)
    -        if (redirectInsecure && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
    -            return res.redirect(301, 'https://' + req.headers['host'] + req.url);
    -        }
    -
             // Ensure that the certificate was validated at the protocol level
             if (!req.socket?.authorized) {
                 const authError = req.socket?.authorizationError || 'unknown';
    
  • lib/clientCertificateAuth.d.cts+1 7 modified
    @@ -47,20 +47,14 @@ export interface ClientCertResponse extends ServerResponse {
     }
     
     export interface ClientCertificateAuthOptions {
    -    /**
    -     * If true, redirect HTTP requests to HTTPS.
    -     * WARNING: This can expose the initial request to MITM attacks.
    -     * @default false
    -     */
    -    redirectInsecure?: boolean;
    -
         /**
          * Use a preset configuration for a known reverse proxy.
          * Header-based certs are only checked if this or certificateHeader is set.
          * @see https://github.com/tgies/client-certificate-auth#reverse-proxy-support
          */
         certificateSource?: CertificateSource;
     
    +
         /**
          * Custom header name to read certificate from.
          * Overrides preset header name if also using certificateSource.
    
  • lib/clientCertificateAuth.d.ts+14 7 modified
    @@ -40,20 +40,14 @@ export interface ClientCertResponse extends ServerResponse {
     }
     
     export interface ClientCertificateAuthOptions {
    -    /**
    -     * If true, redirect HTTP requests to HTTPS.
    -     * WARNING: This can expose the initial request to MITM attacks.
    -     * @default false
    -     */
    -    redirectInsecure?: boolean;
    -
         /**
          * Use a preset configuration for a known reverse proxy.
          * Header-based certs are only checked if this or certificateHeader is set.
          * @see https://github.com/tgies/client-certificate-auth#reverse-proxy-support
          */
         certificateSource?: CertificateSource;
     
    +
         /**
          * Custom header name to read certificate from.
          * Overrides preset header name if also using certificateSource.
    @@ -79,6 +73,19 @@ export interface ClientCertificateAuthOptions {
          * @default false
          */
         includeChain?: boolean;
    +
    +    /**
    +     * Header name containing certificate verification status from upstream proxy.
    +     * Must be used together with verifyValue. Example: 'X-SSL-Client-Verify' for nginx.
    +     */
    +    verifyHeader?: string;
    +
    +    /**
    +     * Expected value indicating successful certificate verification.
    +     * If verifyHeader is set, requests are rejected unless the header matches this value.
    +     * Example: 'SUCCESS' for nginx.
    +     */
    +    verifyValue?: string;
     }
     
     export type ValidationCallback = (cert: PeerCertificate | DetailedPeerCertificate) => boolean | Promise<boolean>;
    
  • lib/clientCertificateAuth.js+15 8 modified
    @@ -15,8 +15,6 @@ import { getCertificateFromHeaders } from './parsers.js';
     
     /**
      * @typedef {Object} ClientCertificateAuthOptions
    - * @property {boolean} [redirectInsecure=false] - If true, redirect HTTP requests to HTTPS. 
    - *   WARNING: This can expose the initial request to MITM attacks.
      * @property {'aws-alb' | 'envoy' | 'cloudflare' | 'traefik'} [certificateSource] - Use a preset 
      *   configuration for a known reverse proxy. Header-based certs are only checked if this or
      *   certificateHeader is set.
    @@ -28,6 +26,10 @@ import { getCertificateFromHeaders } from './parsers.js';
      *   fails (header absent or malformed), try socket.getPeerCertificate() instead of returning 401.
      * @property {boolean} [includeChain=false] - If true, include the full certificate chain via
      *   cert.issuerCertificate. Applies to both socket and header-based extraction.
    + * @property {string} [verifyHeader] - Header name containing certificate verification status from
    + *   upstream proxy (e.g., 'X-SSL-Client-Verify'). Must be used with verifyValue.
    + * @property {string} [verifyValue] - Expected value indicating successful verification (e.g., 'SUCCESS').
    + *   If verifyHeader is set, requests are rejected unless the header matches this value.
      */
     
     /**
    @@ -63,27 +65,32 @@ import { getCertificateFromHeaders } from './parsers.js';
      */
     export default function clientCertificateAuth(callback, options = {}) {
       const {
    -    redirectInsecure = false,
         certificateSource,
         certificateHeader,
         headerEncoding,
         fallbackToSocket = false,
         includeChain = false,
    +    verifyHeader,
    +    verifyValue,
       } = options;
     
       // Determine if header-based extraction is configured
       const useHeaders = Boolean(certificateSource || certificateHeader);
     
       return function middleware(req, res, next) {
    -    // Optional HTTPS redirect (opt-in only due to security concerns)
    -    if (redirectInsecure && !req.secure && req.headers['x-forwarded-proto'] !== 'https') {
    -      return res.redirect(301, 'https://' + req.headers['host'] + req.url);
    -    }
    -
         let cert = null;
     
         // Try header-based extraction first if configured
         if (useHeaders) {
    +      // Verify upstream proxy's certificate validation if configured
    +      if (verifyHeader && verifyValue) {
    +        const verifyStatus = req.headers[verifyHeader.toLowerCase()];
    +        if (verifyStatus !== verifyValue) {
    +          const e = new Error(`Unauthorized: Certificate verification failed (${verifyStatus || 'header missing'})`);
    +          e.status = 401;
    +          return next(e);
    +        }
    +      }
           cert = getCertificateFromHeaders(req.headers, {
             certificateSource,
             certificateHeader,
    
  • README.md+22 4 modified
    @@ -119,12 +119,13 @@ Returns Express middleware.
     | Name | Type | Description |
     |------|------|-------------|
     | `callback` | `(cert) => boolean \| Promise<boolean>` | Receives the client certificate, returns `true` to allow access |
    -| `options.redirectInsecure` | `boolean` | If `true`, redirect HTTP → HTTPS (default: `false`) |
     | `options.certificateSource` | `string` | Use a preset for a known proxy: `'aws-alb'`, `'envoy'`, `'cloudflare'`, `'traefik'` |
     | `options.certificateHeader` | `string` | Custom header name to read certificate from |
     | `options.headerEncoding` | `string` | Encoding format: `'url-pem'`, `'url-pem-aws'`, `'xfcc'`, `'base64-der'`, `'rfc9440'` |
     | `options.fallbackToSocket` | `boolean` | If header extraction fails, try `socket.getPeerCertificate()` (default: `false`) |
     | `options.includeChain` | `boolean` | If `true`, include full certificate chain via `cert.issuerCertificate` (default: `false`) |
    +| `options.verifyHeader` | `string` | Header name containing verification status from proxy (e.g., `'X-SSL-Client-Verify'`) |
    +| `options.verifyValue` | `string` | Expected value indicating successful verification (e.g., `'SUCCESS'`) |
     
     **Certificate Object:**
     
    @@ -263,12 +264,29 @@ Configure your proxy to:
     2. **Set** the header only for authenticated mTLS connections
     3. **Never** trust certificate headers from untrusted sources
     
    +#### Verification Header (Defense in Depth)
    +
    +For additional protection, use `verifyHeader` and `verifyValue` to validate that your proxy has actually verified the certificate. This guards against proxy misconfiguration (e.g., `ssl_verify_client optional` passing unverified certs):
    +
    +```javascript
    +app.use(clientCertificateAuth(checkAuth, {
    +  certificateHeader: 'X-SSL-Client-Cert',
    +  headerEncoding: 'url-pem',
    +  verifyHeader: 'X-SSL-Client-Verify',
    +  verifyValue: 'SUCCESS'
    +}));
    +```
    +
     Example nginx configuration:
     ```nginx
    -# Strip any existing header from clients
    +# Strip any existing headers from clients
     proxy_set_header X-SSL-Client-Cert "";
    +proxy_set_header X-SSL-Client-Verify "";
    +
    +# Always send verification status
    +proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
     
    -# Set header only when client cert is verified
    +# Only send cert if verified
     if ($ssl_client_verify = SUCCESS) {
         proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
     }
    @@ -391,9 +409,9 @@ app.use(clientCertificateAuth((cert) => cert.subject.CN === 'admin'));
     
     ## Security Notes
     
    -- **Do not use `redirectInsecure: true` in production** — the initial HTTP request can be intercepted
     - Set `rejectUnauthorized: false` on your HTTPS server to let this middleware provide helpful error messages, rather than dropping connections silently
     - **When using header-based auth**, ensure your proxy strips certificate headers from external requests
    +- Use `verifyHeader`/`verifyValue` as defense-in-depth when using header-based authentication
     
     ## License
     
    
  • test/docker/backend/server.js+17 0 modified
    @@ -50,6 +50,23 @@ const HELPER_CONFIGS = {
             headerConfig: PATH_CONFIGS['/nginx'],
             validator: anyOf(allowCN(['Wrong Client']), allowCN(['Test Client'])),
         },
    +    // Verification header test routes
    +    '/helpers/verify-header': {
    +        headerConfig: {
    +            ...PATH_CONFIGS['/nginx'],
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'SUCCESS',
    +        },
    +        validator: () => true,
    +    },
    +    '/helpers/verify-header-wrong-value': {
    +        headerConfig: {
    +            ...PATH_CONFIGS['/nginx'],
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'WRONG_VALUE', // This should always fail
    +        },
    +        validator: () => true,
    +    },
     };
     
     const server = http.createServer((req, res) => {
    
  • test/docker/nginx/nginx-optional.conf+20 0 modified
    @@ -18,13 +18,33 @@ http {
             ssl_client_certificate /certs/ca.pem;
             ssl_verify_client optional_no_ca;
     
    +        # Helper routes (preserve path for verification header testing)
    +        location /helpers/ {
    +            proxy_pass http://backend:3000;
    +            
    +            # Forward client certificate as URL-encoded PEM (if present)
    +            proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
    +            
    +            # Forward certificate verification status (SUCCESS, FAILED, NONE)
    +            proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
    +            
    +            # Standard headers
    +            proxy_set_header Host $host;
    +            proxy_set_header X-Real-IP $remote_addr;
    +            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    +            proxy_set_header X-Forwarded-Proto $scheme;
    +        }
    +
             location / {
                 # Forward to backend with /nginx path for routing
                 proxy_pass http://backend:3000/nginx;
                 
                 # Forward client certificate as URL-encoded PEM (if present)
                 proxy_set_header X-SSL-Client-Cert $ssl_client_escaped_cert;
                 
    +            # Forward certificate verification status (SUCCESS, FAILED, NONE)
    +            proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
    +            
                 # Standard headers
                 proxy_set_header Host $host;
                 proxy_set_header X-Real-IP $remote_addr;
    
  • test/test-e2e-proxies.js+29 0 modified
    @@ -472,6 +472,35 @@ describeIfDocker('Reverse Proxy Integration Tests', () => {
                 });
             });
         });
    +
    +    // Verification Header E2E Tests (using nginx-optional which sends X-SSL-Client-Verify)
    +    describe('Verification Header', () => {
    +        it('should accept request when verifyHeader matches verifyValue', async () => {
    +            const response = await makeRequest(
    +                `https://localhost:${NGINX_OPTIONAL_PORT}/helpers/verify-header`,
    +                certs.client.cert,
    +                certs.client.key,
    +                certs.ca.cert
    +            );
    +
    +            expect(response.success).toBe(true);
    +            expect(response.clientCN).toBe('Test Client');
    +            expect(response.helperPath).toBe('/helpers/verify-header');
    +        });
    +
    +        it('should reject request when verifyValue does not match', async () => {
    +            const response = await makeRequest(
    +                `https://localhost:${NGINX_OPTIONAL_PORT}/helpers/verify-header-wrong-value`,
    +                certs.client.cert,
    +                certs.client.key,
    +                certs.ca.cert
    +            );
    +
    +            expect(response.success).toBe(false);
    +            expect(response.status).toBe(401);
    +            expect(response.error).toContain('Certificate verification failed');
    +        });
    +    });
     });
     
     function makeRequestWithoutCert(url, caCert) {
    
  • test/test-unit-clientCertificateAuth.cjs+0 62 modified
    @@ -63,11 +63,6 @@ describe('clientCertificateAuth (CommonJS)', () => {
                 headers: {}
             };
     
    -        const mockUnsecureReq = {
    -            secure: false,
    -            socket: { authorized: false, getPeerCertificate: () => ({}) },
    -            headers: {}
    -        };
     
             const mockUnauthReq = {
                 secure: true,
    @@ -258,63 +253,6 @@ describe('clientCertificateAuth (CommonJS)', () => {
                 });
             });
     
    -        describe('redirectInsecure option', () => {
    -            it('should NOT redirect by default', done => {
    -                let redirectCalled = false;
    -                const res = {
    -                    redirect: () => { redirectCalled = true; }
    -                };
    -                const middleware = clientCertificateAuth(() => true);
    -                middleware(mockUnsecureReq, res, (err) => {
    -                    assert.equal(redirectCalled, false);
    -                    assert.ok(err instanceof Error);
    -                    done();
    -                });
    -            });
    -
    -            it('should redirect when redirectInsecure is true', () => {
    -                let redirectUrl = null;
    -                let redirectStatus = null;
    -                const req = {
    -                    secure: false,
    -                    socket: {},
    -                    headers: {
    -                        'host': 'example.com'
    -                    },
    -                    url: '/protected'
    -                };
    -                const res = {
    -                    redirect: (status, url) => {
    -                        redirectStatus = status;
    -                        redirectUrl = url;
    -                    }
    -                };
    -                const middleware = clientCertificateAuth(() => true, { redirectInsecure: true });
    -                middleware(req, res, () => { });
    -
    -                assert.equal(redirectStatus, 301);
    -                assert.equal(redirectUrl, 'https://example.com/protected');
    -            });
    -
    -            it('should NOT redirect if x-forwarded-proto is https', done => {
    -                const req = {
    -                    secure: false,
    -                    socket: { authorized: true, getPeerCertificate: getMockPeerCertificate },
    -                    headers: {
    -                        'x-forwarded-proto': 'https'
    -                    }
    -                };
    -                let redirectCalled = false;
    -                const res = {
    -                    redirect: () => { redirectCalled = true; }
    -                };
    -                const middleware = clientCertificateAuth(() => true, { redirectInsecure: true });
    -                middleware(req, res, () => {
    -                    assert.equal(redirectCalled, false);
    -                    done();
    -                });
    -            });
    -        });
     
             describe('includeChain option', () => {
                 const getMockIssuerCertificate = () => ({
    
  • test/test-unit-clientCertificateAuth.js+100 63 modified
    @@ -49,11 +49,6 @@ describe('clientCertificateAuth', () => {
           headers: {}
         };
     
    -    const mockUnsecureReq = {
    -      secure: false,
    -      socket: { authorized: false, getPeerCertificate: () => ({}) },
    -      headers: {}
    -    };
     
         const mockUnauthReq = {
           secure: true,
    @@ -244,64 +239,6 @@ describe('clientCertificateAuth', () => {
           });
         });
     
    -    describe('redirectInsecure option', () => {
    -      it('should NOT redirect by default', done => {
    -        let redirectCalled = false;
    -        const res = {
    -          redirect: () => { redirectCalled = true; }
    -        };
    -        const middleware = clientCertificateAuth(() => true);
    -        middleware(mockUnsecureReq, res, (err) => {
    -          assert.equal(redirectCalled, false);
    -          // Should fail due to unauthorized, not redirect
    -          assert.ok(err instanceof Error);
    -          done();
    -        });
    -      });
    -
    -      it('should redirect when redirectInsecure is true', () => {
    -        let redirectUrl = null;
    -        let redirectStatus = null;
    -        const req = {
    -          secure: false,
    -          socket: {},
    -          headers: {
    -            'host': 'example.com'
    -          },
    -          url: '/protected'
    -        };
    -        const res = {
    -          redirect: (status, url) => {
    -            redirectStatus = status;
    -            redirectUrl = url;
    -          }
    -        };
    -        const middleware = clientCertificateAuth(() => true, { redirectInsecure: true });
    -        middleware(req, res, () => { });
    -
    -        assert.equal(redirectStatus, 301);
    -        assert.equal(redirectUrl, 'https://example.com/protected');
    -      });
    -
    -      it('should NOT redirect if x-forwarded-proto is https', done => {
    -        const req = {
    -          secure: false,
    -          socket: { authorized: true, getPeerCertificate: getMockPeerCertificate },
    -          headers: {
    -            'x-forwarded-proto': 'https'
    -          }
    -        };
    -        let redirectCalled = false;
    -        const res = {
    -          redirect: () => { redirectCalled = true; }
    -        };
    -        const middleware = clientCertificateAuth(() => true, { redirectInsecure: true });
    -        middleware(req, res, () => {
    -          assert.equal(redirectCalled, false);
    -          done();
    -        });
    -      });
    -    });
     
         describe('header-based certificate extraction', () => {
           let testPem;
    @@ -408,6 +345,106 @@ describe('clientCertificateAuth', () => {
               done();
             });
           });
    +
    +      describe('verifyHeader/verifyValue options', () => {
    +        it('should reject if verifyHeader is set but header is missing', done => {
    +          const encodedCert = encodeURIComponent(testPem);
    +          const req = {
    +            secure: false,
    +            socket: { authorized: false },
    +            headers: {
    +              'x-ssl-client-cert': encodedCert
    +              // X-SSL-Client-Verify header is missing
    +            }
    +          };
    +
    +          const middleware = clientCertificateAuth(() => true, {
    +            certificateHeader: 'X-SSL-Client-Cert',
    +            headerEncoding: 'url-pem',
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'SUCCESS'
    +          });
    +
    +          middleware(req, mockRes, (err) => {
    +            assert.ok(err instanceof Error);
    +            assert.equal(err.status, 401);
    +            assert.ok(err.message.includes('Certificate verification failed'));
    +            assert.ok(err.message.includes('header missing'));
    +            done();
    +          });
    +        });
    +
    +        it('should reject if verifyHeader value does not match verifyValue', done => {
    +          const encodedCert = encodeURIComponent(testPem);
    +          const req = {
    +            secure: false,
    +            socket: { authorized: false },
    +            headers: {
    +              'x-ssl-client-cert': encodedCert,
    +              'x-ssl-client-verify': 'FAILED:unable to verify'
    +            }
    +          };
    +
    +          const middleware = clientCertificateAuth(() => true, {
    +            certificateHeader: 'X-SSL-Client-Cert',
    +            headerEncoding: 'url-pem',
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'SUCCESS'
    +          });
    +
    +          middleware(req, mockRes, (err) => {
    +            assert.ok(err instanceof Error);
    +            assert.equal(err.status, 401);
    +            assert.ok(err.message.includes('Certificate verification failed'));
    +            assert.ok(err.message.includes('FAILED:unable to verify'));
    +            done();
    +          });
    +        });
    +
    +        it('should allow request if verifyHeader matches verifyValue', done => {
    +          const encodedCert = encodeURIComponent(testPem);
    +          const req = {
    +            secure: false,
    +            socket: { authorized: false },
    +            headers: {
    +              'x-ssl-client-cert': encodedCert,
    +              'x-ssl-client-verify': 'SUCCESS'
    +            }
    +          };
    +
    +          const middleware = clientCertificateAuth((cert) => {
    +            assert.equal(cert.subject.CN, 'Header Test Client');
    +            return true;
    +          }, {
    +            certificateHeader: 'X-SSL-Client-Cert',
    +            headerEncoding: 'url-pem',
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'SUCCESS'
    +          });
    +
    +          middleware(req, mockRes, (err) => {
    +            assert.equal(err, undefined);
    +            done();
    +          });
    +        });
    +
    +        it('should not check verifyHeader for socket-based extraction', done => {
    +          // Socket-based auth should ignore verifyHeader
    +          const middleware = clientCertificateAuth((cert) => {
    +            assert.equal(cert.subject.CN, 'Proctor Davenport');
    +            return true;
    +          }, {
    +            verifyHeader: 'X-SSL-Client-Verify',
    +            verifyValue: 'SUCCESS'
    +            // No certificateSource or certificateHeader = socket-based
    +          });
    +
    +          middleware(mockGoodReq, mockRes, (err) => {
    +            assert.equal(err, undefined);
    +            done();
    +          });
    +        });
    +      });
         });
     
         describe('req.clientCertificate', () => {
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.