@envseal/detector 0.1.2 → 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.
Files changed (2) hide show
  1. package/dist/exclusions.js +61 -10
  2. package/package.json +2 -2
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envseal/detector",
3
- "version": "0.1.2",
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.2"
23
+ "@envseal/registry": "0.1.3"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",