flecto 3.0.1 → 3.1.0

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.
package/src/secrets.js CHANGED
@@ -57,15 +57,22 @@ const KNOWN_FORMATS = [
57
57
  { kind: 'stripe-secret-key', re: /\b[sr]k_live_[0-9A-Za-z]{16,}\b/g },
58
58
  // JWT: base64url header starting with "eyJ" ('{"'), payload, signature.
59
59
  { kind: 'jwt', re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g },
60
- // PEM private key blocks, including PGP blocks and unterminated fragments.
61
- {
62
- kind: 'private-key-block',
63
- re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|$)/g,
64
- },
65
60
  ];
66
61
 
67
- /** Credentials embedded in a URL authority: scheme://user:PASSWORD@host. */
68
- const URL_CREDENTIALS_RE = /[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:([^\s/@]+)@/gi;
62
+ // PEM/PGP private-key block markers, matched linearly. A single regex spanning
63
+ // BEGIN…END with `[\s\S]*?…$` backtracks quadratically on a long BEGIN-prefixed
64
+ // value with no END — a denial-of-service vector, since secret detection runs
65
+ // on every changed string value. Instead we find the markers with anchored,
66
+ // non-spanning regexes and pair them by position (see findPrivateKeyBlocks).
67
+ const PRIVATE_KEY_BEGIN_RE = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
68
+ const PRIVATE_KEY_END_RE = /-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
69
+
70
+ // Credentials embedded in a URL authority: scheme://user:PASSWORD@host. The
71
+ // scheme run is length-bounded ({0,32}); an unbounded `*` before the required
72
+ // `://` backtracks quadratically on a long value that never contains `://`. No
73
+ // real URL scheme approaches 32 characters, so the bound changes nothing that
74
+ // matters and removes the ReDoS.
75
+ const URL_CREDENTIALS_RE = /[a-z][a-z0-9+.-]{0,32}:\/\/[^\s/:@]+:([^\s/@]+)@/gi;
69
76
 
70
77
  /**
71
78
  * Values that only *reference* a secret. Redacting these adds noise and, worse,
@@ -205,6 +212,31 @@ function isHighEntropySecret(value) {
205
212
  return true;
206
213
  }
207
214
 
215
+ /**
216
+ * PEM/PGP private-key spans, found linearly. Each BEGIN marker pairs with the
217
+ * next END marker after it; a BEGIN with no following END runs to end of string
218
+ * (an unterminated fragment is still a leaked key). Pairing by position avoids
219
+ * the quadratic backtracking a single BEGIN…END regex incurs.
220
+ * @param {string} value
221
+ * @returns {SecretMatch[]}
222
+ */
223
+ function findPrivateKeyBlocks(value) {
224
+ /** @type {SecretMatch[]} */
225
+ const spans = [];
226
+ PRIVATE_KEY_BEGIN_RE.lastIndex = 0;
227
+ let begin;
228
+ while ((begin = PRIVATE_KEY_BEGIN_RE.exec(value)) !== null) {
229
+ const bodyStart = begin.index + begin[0].length;
230
+ PRIVATE_KEY_END_RE.lastIndex = bodyStart;
231
+ const end = PRIVATE_KEY_END_RE.exec(value);
232
+ const spanEnd = end ? end.index + end[0].length : value.length;
233
+ spans.push({ kind: 'private-key-block', start: begin.index, end: spanEnd });
234
+ // Resume scanning past this block so overlapping BEGINs inside it are skipped.
235
+ PRIVATE_KEY_BEGIN_RE.lastIndex = spanEnd;
236
+ }
237
+ return spans;
238
+ }
239
+
208
240
  /**
209
241
  * Locate every secret-shaped span inside a string, sorted and non-overlapping.
210
242
  * @param {string} value
@@ -223,6 +255,8 @@ function findSecretMatches(value) {
223
255
  }
224
256
  }
225
257
 
258
+ matches.push(...findPrivateKeyBlocks(value));
259
+
226
260
  URL_CREDENTIALS_RE.lastIndex = 0;
227
261
  let credentials;
228
262
  while ((credentials = URL_CREDENTIALS_RE.exec(value)) !== null) {
@@ -296,6 +330,33 @@ export function redactSecretString(value) {
296
330
  return out + value.slice(cursor);
297
331
  }
298
332
 
333
+ /**
334
+ * Key names that mean "this value is a credential".
335
+ *
336
+ * Matched against the *configuration* path only. A multi-document file prefixes
337
+ * every path with the document's identity — `Deployment/prod/token-service.…` —
338
+ * and that identity is a resource name the user chose, not a key name. Letting
339
+ * it match masks every value in the document, numbers and booleans included, so
340
+ * callers strip the document prefix before matching: see `secretMatchPath` in
341
+ * differ.js, and the document handling in `maskState`.
342
+ */
343
+ export const SECRET_PATH_RE = /(secret|token|password|api[_-]?key|private[_-]?key|credential)/i;
344
+
345
+ /**
346
+ * True when a configuration path names a credential.
347
+ *
348
+ * Lives here rather than beside either caller because there are two of them —
349
+ * the renderer masking a diff for display, and the snapshot store masking a
350
+ * state for a commit — and a store that recognized fewer key names than the
351
+ * terminal did would write into git history exactly the values the terminal
352
+ * thought were too sensitive to print.
353
+ * @param {string} path
354
+ * @returns {boolean}
355
+ */
356
+ export function looksLikeSecretPath(path) {
357
+ return SECRET_PATH_RE.test(path);
358
+ }
359
+
299
360
  /**
300
361
  * True when a value — or any string nested inside a plain object or array —
301
362
  * looks like a secret.