@envseal/detector 0.1.1 → 0.1.3

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.
@@ -73,34 +73,85 @@ function isUUID(candidate) {
73
73
  const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
74
74
  return uuidPattern.test(candidate);
75
75
  }
76
+ /**
77
+ * Audit fix: these buckets previously excluded on PREFIX alone (`sha256:` +
78
+ * anything), letting attacker-controlled context park a real credential
79
+ * behind an algorithm label or an unvalidated `integrity=` shape. Each now
80
+ * demands a base64/hex body of plausible digest length for its algorithm.
81
+ */
82
+ const DIGEST_BODY_FLOOR = {
83
+ 'sha256:': 43,
84
+ 'sha256-': 43,
85
+ 'sha512-': 86,
86
+ 'sha1-': 27,
87
+ 'md5-': 22,
88
+ };
76
89
  function isDigest(candidate, context) {
77
- if (candidate.startsWith('sha256:') || candidate.startsWith('sha256-') ||
78
- candidate.startsWith('sha512-') || candidate.startsWith('sha1-') ||
79
- candidate.startsWith('md5-')) {
80
- return true;
90
+ // Standard-base64/hex charset only: SRI and docker-style digest bodies
91
+ // never contain `-` or `_`, but vendor key shapes (sk-proj-*, ghp-*) do.
92
+ // That charset distinction — plus the length floor — is what keeps a real
93
+ // credential from hiding behind an algorithm label.
94
+ const DIGEST_BODY = /^[A-Za-z0-9+/=]+$/;
95
+ for (const [prefix, floor] of Object.entries(DIGEST_BODY_FLOOR)) {
96
+ if (candidate.startsWith(prefix)) {
97
+ const body = candidate.slice(prefix.length);
98
+ return (DIGEST_BODY.test(body) && body.length >= floor && body.length <= 88);
99
+ }
81
100
  }
82
101
  if (context.before.endsWith('integrity=') || context.before.endsWith('integrity="')) {
83
- return /^[A-Za-z0-9+/=]+$/.test(candidate);
102
+ // SRI values carry their own algorithm prefix; bare base64 in this
103
+ // position is not an SRI hash and gets no exclusion.
104
+ return /^sha(256|384|512)-[A-Za-z0-9+/=]+$/.test(candidate);
84
105
  }
85
106
  return false;
86
107
  }
87
108
  function isDataUri(candidate, context) {
88
- if (candidate.startsWith('data:'))
109
+ // Audit fix: was prefix-only. A data: URI must now look like one — a MIME
110
+ // type, and if base64 is claimed, a decodable-length body — before its
111
+ // content can hide from detection.
112
+ const match = /^data:([a-z0-9.+-]+\/[a-z0-9.+-]+)?(;?[a-z0-9-]+=[a-z0-9-]+)*(;base64,)?(.*)$/i.exec(candidate);
113
+ if (match !== null) {
114
+ const [, mime, , base64Marker, body] = match;
115
+ if ((mime ?? '') === '')
116
+ return false; // `data:` alone proves nothing
117
+ if (base64Marker !== undefined) {
118
+ return /^[A-Za-z0-9+/=]*$/.test(body ?? '') && (body ?? '').length % 4 === 0 && (body ?? '').length > 0;
119
+ }
89
120
  return true;
90
- return /data:[^/]*\/[^;]*;base64,$/.test(context.before);
121
+ }
122
+ return /data:[a-z0-9.+-]+\/[a-z0-9.+-]+(;[a-z0-9-]+=[a-z0-9-]+)*;base64,[A-Za-z0-9+/=]+$/.test(context.before);
91
123
  }
92
124
  function isFilesystemPath(candidate) {
93
125
  const hasPathSep = candidate.includes('/') || candidate.includes(String.fromCharCode(92));
94
126
  if (!hasPathSep)
95
127
  return false;
96
128
  const hasCredential = /[a-z][a-z0-9+.-]*:\/\/[^@]*:.*@/.test(candidate);
97
- return !hasCredential;
129
+ if (hasCredential)
130
+ return false;
131
+ // Audit fix: any slash-containing string used to be excluded, but ~27% of
132
+ // random base64 credentials contain `/`. Require actual path structure:
133
+ // an anchored form or a path-like extension — not merely a slash.
134
+ const pathlike = /^\.\.?[/\\]/.test(candidate) || // ./x, ../x
135
+ /^[/\\]/.test(candidate) || // absolute POSIX
136
+ /^[A-Za-z]:[/\\]/.test(candidate) || // drive path
137
+ /\.[A-Za-z][A-Za-z0-9]{0,7}$/.test(candidate); // ends in an extension
138
+ return pathlike;
98
139
  }
99
140
  function isPlainUrl(candidate) {
100
141
  try {
101
142
  const url = new URL(candidate);
102
- return (url.protocol === 'http:' || url.protocol === 'https:') &&
103
- url.username === '' && url.password === '';
143
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
144
+ return false;
145
+ }
146
+ if (url.username !== '' || url.password !== '') {
147
+ return false;
148
+ }
149
+ // Audit fix: URLs whose query carries credential-shaped parameters are a
150
+ // real delivery channel for keys (Google-style ?key=...), not plain links.
151
+ if (/[?&](key|token|secret|password|passwd|api[-_]?key|access[-_]?token|sig|signature|credential)s?=/i.test(url.search)) {
152
+ return false;
153
+ }
154
+ return true;
104
155
  }
105
156
  catch {
106
157
  return false;
package/dist/patterns.js CHANGED
@@ -5,6 +5,11 @@ function escapeRegExp(s) {
5
5
  const SW = '[A-Za-z0-9_\\-]';
6
6
  const HAND_PATTERNS = [
7
7
  { id: 'openai-project-key', label: 'OpenAI project API key', providerId: 'openai', source: `sk-proj-${SW}{20,}` },
8
+ // F5: legacy OpenAI live keys (`sk-live-…`) and bare `sk-` runs were not
9
+ // covered — the W4 battle-test pasted `KEY=sk-live-w4bt…` straight through
10
+ // the user-prompt redactor. Same shape family as sk-proj-, so same guard.
11
+ { id: 'openai-legacy-key', label: 'OpenAI API key', providerId: 'openai', source: `sk-live-${SW}{20,}` },
12
+ { id: 'openai-generic-key', label: 'possible OpenAI API key', providerId: 'openai', source: `sk-(?!proj-|live-)[A-Za-z0-9]{20,}` },
8
13
  { id: 'anthropic-api03', label: 'Anthropic API key', providerId: 'anthropic', source: `sk-ant-api03-${SW}{20,}` },
9
14
  { id: 'github-pat', label: 'GitHub personal access token', providerId: 'github', source: `ghp_${SW}{30,}` },
10
15
  { id: 'github-oauth', label: 'GitHub OAuth access token', providerId: 'github', source: `gho_${SW}{30,}` },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envseal/detector",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  "provenance": true
21
21
  },
22
22
  "dependencies": {
23
- "@envseal/registry": "0.1.1"
23
+ "@envseal/registry": "0.1.3"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",