@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.
- package/dist/exclusions.js +61 -10
- package/package.json +2 -2
package/dist/exclusions.js
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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.
|
|
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.
|
|
23
|
+
"@envseal/registry": "0.1.3"
|
|
24
24
|
},
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|