client-certificate-auth 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,19 @@ function isThenable(value) {
32
32
  && typeof value.then === 'function';
33
33
  }
34
34
 
35
+ /**
36
+ * Wrap primitive values thrown or rejected by the validation callback in an
37
+ * Error so `.message` and `.status` access is safe; objects pass through.
38
+ * @param {unknown} err
39
+ * @returns {any}
40
+ */
41
+ function normalizeCallbackError(err) {
42
+ if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
43
+ return err;
44
+ }
45
+ return new Error(err === null || err === undefined ? '' : String(err));
46
+ }
47
+
35
48
  /**
36
49
  * Options not supported by the sync CJS wrapper.
37
50
  * These require the ESM module's async header parsing.
@@ -132,7 +145,8 @@ function clientCertificateAuth(callback, options = {}) {
132
145
  try {
133
146
  const result = callback(cert, req);
134
147
  if (isThenable(result)) {
135
- Promise.resolve(result).then(doneAuthorizing).catch((err) => {
148
+ Promise.resolve(result).then(doneAuthorizing).catch((rejection) => {
149
+ const err = normalizeCallbackError(rejection);
136
150
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
137
151
  if (err.status === undefined) {
138
152
  err.status = 401;
@@ -142,7 +156,8 @@ function clientCertificateAuth(callback, options = {}) {
142
156
  } else {
143
157
  doneAuthorizing(result);
144
158
  }
145
- } catch (err) {
159
+ } catch (thrown) {
160
+ const err = normalizeCallbackError(thrown);
146
161
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
147
162
  if (err.status === undefined) {
148
163
  err.status = 401;
@@ -20,6 +20,19 @@ function isThenable(value) {
20
20
  && typeof /** @type {{then?: unknown}} */ (value).then === 'function';
21
21
  }
22
22
 
23
+ /**
24
+ * Wrap primitive values thrown or rejected by the validation callback in an
25
+ * Error so `.message` and `.status` access is safe; objects pass through.
26
+ * @param {unknown} err
27
+ * @returns {any}
28
+ */
29
+ function normalizeCallbackError(err) {
30
+ if (err !== null && (typeof err === 'object' || typeof err === 'function')) {
31
+ return err;
32
+ }
33
+ return new Error(err === null || err === undefined ? '' : String(err));
34
+ }
35
+
23
36
  /**
24
37
  * @typedef {import('http').IncomingMessage & { secure?: boolean; socket: import('tls').TLSSocket & { authorized?: boolean; authorizationError?: string }; clientCertificate?: import('tls').PeerCertificate }} ClientCertRequest
25
38
  * @typedef {import('http').ServerResponse & { redirect: (statusOrUrl: number | string, url?: string) => void }} ClientCertResponse
@@ -191,7 +204,8 @@ export default function clientCertificateAuth(callback, options = {}) {
191
204
  try {
192
205
  const callbackResult = callback(cert, req);
193
206
  if (isThenable(callbackResult)) {
194
- Promise.resolve(callbackResult).then(doneAuthorizing).catch((err) => {
207
+ Promise.resolve(callbackResult).then(doneAuthorizing).catch((rejection) => {
208
+ const err = normalizeCallbackError(rejection);
195
209
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
196
210
  if (err.status === undefined) {
197
211
  err.status = 401;
@@ -201,7 +215,8 @@ export default function clientCertificateAuth(callback, options = {}) {
201
215
  } else {
202
216
  doneAuthorizing(callbackResult);
203
217
  }
204
- } catch (err) {
218
+ } catch (thrown) {
219
+ const err = normalizeCallbackError(thrown);
205
220
  safeCallHook(onRejected, cert, req, err.message || 'callback_threw');
206
221
  if (err.status === undefined) {
207
222
  err.status = 401;
package/lib/parsers.js CHANGED
@@ -103,8 +103,9 @@ function splitPemBlocks(pem) {
103
103
 
104
104
  /**
105
105
  * Parse a multi-block PEM blob into a chained PeerCertificate. Splits the
106
- * input into individual blocks, parses each, drops blocks that fail to
107
- * parse, and links the remainder via issuerCertificate.
106
+ * input into individual blocks and links them via issuerCertificate. The
107
+ * first block is the leaf and must parse; later blocks that fail to parse
108
+ * are dropped and the chain links past them.
108
109
  *
109
110
  * Used by parsers whose proxies forward the full chain in a single field
110
111
  * (parseUrlPemAws, parseXfcc Chain). Without explicit chain linking, calling
@@ -117,22 +118,33 @@ function splitPemBlocks(pem) {
117
118
  */
118
119
  function chainFromMultiBlockPem(pem) {
119
120
  const pemBlocks = splitPemBlocks(pem);
120
- // Stryker disable next-line BlockStatement,ConditionalExpression: short-circuit; falling through hits the next certs.length===0 check with the same null result
121
+ // Stryker disable next-line BlockStatement,ConditionalExpression: short-circuit; falling through hands undefined to pemToCertificate, which throws for the same null result
121
122
  if (pemBlocks.length === 0) {
122
123
  return null;
123
124
  }
124
125
 
125
- const certs = pemBlocks.map(block => {
126
+ // Junk ahead of the leaf block means the leaf was damaged past recognition;
127
+ // the next block must not slide into its place. Leading whitespace is fine.
128
+ if (pem.slice(0, pem.indexOf(pemBlocks[0])).trim() !== '') {
129
+ return null;
130
+ }
131
+
132
+ // An unparseable leaf rejects the whole blob. Promoting the next block
133
+ // would authenticate the request as whichever certificate parsed first.
134
+ let leaf;
135
+ try {
136
+ leaf = pemToCertificate(pemBlocks[0]);
137
+ } catch {
138
+ return null;
139
+ }
140
+
141
+ const certs = [leaf];
142
+ for (const block of pemBlocks.slice(1)) {
126
143
  try {
127
- return pemToCertificate(block);
144
+ certs.push(pemToCertificate(block));
128
145
  } catch {
129
- // Stryker disable next-line BlockStatement: empty catch returns undefined which .filter(Boolean) drops alongside null — same chain output
130
- return null;
146
+ // Dropped; the chain links past it.
131
147
  }
132
- }).filter(Boolean);
133
-
134
- if (certs.length === 0) {
135
- return null;
136
148
  }
137
149
 
138
150
  // Stryker disable next-line EqualityOperator: setting issuerCertificate = undefined on last cert is same as not setting it
@@ -271,27 +283,25 @@ export function parseBase64Der(headerValue) {
271
283
  }
272
284
 
273
285
  // Handle comma-separated cert chains (Traefik format)
274
- // Stryker disable next-line MethodExpression: .trim() no-op (base64 ignores whitespace); .filter(Boolean) redundant (empty strings throw in X509Certificate, caught below)
275
- const certParts = headerValue.split(',').map(s => s.trim()).filter(Boolean);
286
+ // Stryker disable next-line MethodExpression: .trim() no-op (base64 ignores whitespace)
287
+ const certParts = headerValue.split(',').map(s => s.trim());
276
288
 
277
- // Stryker disable next-line BlockStatement,ConditionalExpression: →false equivalent (empty array → zero certs → certs.length===0 catches it); →true killed by valid base64-der tests
278
- if (certParts.length === 0) {
289
+ // An empty or unparseable leaf rejects the whole header. Promoting the next
290
+ // entry would authenticate the request as whichever certificate parsed first.
291
+ let leaf;
292
+ try {
293
+ leaf = derToCertificate(Buffer.from(certParts[0], 'base64'));
294
+ } catch {
279
295
  return null;
280
296
  }
281
297
 
282
- // Parse all certs in the chain
283
- const certs = certParts.map(base64 => {
298
+ const certs = [leaf];
299
+ for (const base64 of certParts.slice(1)) {
284
300
  try {
285
- const derBuffer = Buffer.from(base64, 'base64');
286
- return derToCertificate(derBuffer);
301
+ certs.push(derToCertificate(Buffer.from(base64, 'base64')));
287
302
  } catch {
288
- // Equivalent mutant (undefined also filtered by .filter(Boolean)) — catch-body BlockStatement unsuppressible via Stryker comments
289
- return null;
303
+ // Dropped; the chain links past it.
290
304
  }
291
- }).filter(Boolean);
292
-
293
- if (certs.length === 0) {
294
- return null;
295
305
  }
296
306
 
297
307
  // Link the cert chain via issuerCertificate
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "client-certificate-auth",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Express/Connect middleware for mTLS client certificate authentication with reverse proxy support (AWS ALB, Envoy, Cloudflare, Traefik)",
5
5
  "homepage": "https://github.com/tgies/client-certificate-auth",
6
6
  "bugs": {
@@ -145,6 +145,7 @@
145
145
  "lib/**/*.d.cts"
146
146
  ],
147
147
  "overrides": {
148
- "fflate": "0.8.2"
148
+ "fflate": "0.8.2",
149
+ "qs": "^6.15.2"
149
150
  }
150
151
  }