verdaccio-security-filter 1.4.0 → 1.4.1
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/README.md +27 -30
- package/build/index.js +29 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -150,47 +150,44 @@ npm link
|
|
|
150
150
|
|
|
151
151
|
## 🔧 How It Works
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
The plugin works in two layers. Configure both: the same settings go under `filters` and under `middlewares`.
|
|
154
154
|
|
|
155
155
|
### Layer 1: Metadata Filtering (`filter_metadata`)
|
|
156
|
-
|
|
157
|
-
1.
|
|
158
|
-
2.
|
|
159
|
-
3.
|
|
160
|
-
4. Filters out specific blocked versions from the versions list for partial blocks
|
|
156
|
+
Runs inside Verdaccio when package metadata is read, so it sees every version:
|
|
157
|
+
1. Removes versions blocked by `blockedVersions`, `versionRangeRules` and `whitelist.versions`
|
|
158
|
+
2. Removes versions rejected by the CVE, license, age and author checks
|
|
159
|
+
3. Blocks the package as a whole when no version is left, or when the package itself is not allowed
|
|
161
160
|
|
|
162
|
-
|
|
161
|
+
npm then resolves ranges against the versions that are left.
|
|
163
162
|
|
|
164
|
-
### Layer 2: Middleware
|
|
165
|
-
|
|
166
|
-
1.
|
|
167
|
-
2.
|
|
168
|
-
3.
|
|
169
|
-
4. Returns **403 Forbidden** for blocked packages with detailed error message
|
|
163
|
+
### Layer 2: Middleware (`register_middlewares`)
|
|
164
|
+
Runs first on every `GET` request:
|
|
165
|
+
1. Blocks packages by name: whitelist, patterns, scopes
|
|
166
|
+
2. Blocks tarball downloads of blocked versions, even if a client has stale metadata or a lockfile
|
|
167
|
+
3. Turns a whole-package block from layer 1 into an HTTP error that carries the reason
|
|
170
168
|
|
|
171
|
-
|
|
169
|
+
### What a client sees
|
|
172
170
|
|
|
173
|
-
|
|
171
|
+
A removed version is simply missing from the metadata. A blocked package or tarball is answered with **403 Forbidden**, and the reason is in the `error` field, which npm prints:
|
|
174
172
|
|
|
175
|
-
- **Metadata filtering (404)** - Makes blocked packages invisible to npm/yarn
|
|
176
|
-
- **Middleware interception (403)** - Blocks tarball downloads even with cached metadata
|
|
177
|
-
- Together, they provide **complete protection** against blocked packages being installed in any way
|
|
178
|
-
|
|
179
|
-
Example flow when a blocked package is requested:
|
|
180
173
|
```
|
|
181
174
|
npm install hawk
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
→ HTTP 404 Not Found: Package blocked by security filter: Not in whitelist
|
|
185
|
-
→ npm ERR! 404 Not Found - GET http://localhost:4873/hawk
|
|
186
|
-
|
|
187
|
-
npm install (with hawk as dependency)
|
|
188
|
-
→ GET /hawk/-/hawk-9.0.1.tgz (tarball request)
|
|
189
|
-
→ [Middleware] Tarball request: hawk/hawk-9.0.1.tgz
|
|
190
|
-
→ [Security Filter] Blocked hawk@9.0.1 - Package is not in whitelist
|
|
191
|
-
→ HTTP 403 Forbidden: {"error": "Package blocked by security filter", "reason": "Package is not in whitelist"}
|
|
175
|
+
npm error code E403
|
|
176
|
+
npm error 403 403 Forbidden - GET http://localhost:4873/hawk - Package blocked by security filter: Package is not in whitelist
|
|
192
177
|
```
|
|
193
178
|
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"error": "Package blocked by security filter: License 'GPL-3.0' is not in allowed list",
|
|
182
|
+
"package": "some-package",
|
|
183
|
+
"reason": "License 'GPL-3.0' is not in allowed list",
|
|
184
|
+
"security": { "blocked": true, "reason": "...", "plugin": { "name": "verdaccio-security-filter", "version": "..." } },
|
|
185
|
+
"timestamp": "2026-01-15T10:30:00.000Z"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The 403 for packages blocked in layer 1 (CVE, license, age, author, fail-closed errors) needs the middleware. With only `filters` configured, such a package is returned with an empty version list and npm reports "No versions available" without a reason.
|
|
190
|
+
|
|
194
191
|
## ⚙️ Configuration
|
|
195
192
|
|
|
196
193
|
### Quick Start (Middleware Only)
|
package/build/index.js
CHANGED
|
@@ -56,7 +56,12 @@ const cve_checker_1 = require("./lib/cve-checker");
|
|
|
56
56
|
const license_checker_1 = require("./lib/license-checker");
|
|
57
57
|
const package_age_checker_1 = require("./lib/package-age-checker");
|
|
58
58
|
const author_checker_1 = require("./lib/author-checker");
|
|
59
|
+
const lru_cache_1 = require("./lib/lru-cache");
|
|
59
60
|
const PLUGIN_VERSION = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
61
|
+
const filterBlocks = new lru_cache_1.LRUCache(1000);
|
|
62
|
+
function forbiddenBody(packageName, reason, extra = {}) {
|
|
63
|
+
return Object.assign(Object.assign({ error: `Package blocked by security filter: ${reason}`, package: packageName, reason }, extra), { timestamp: new Date().toISOString() });
|
|
64
|
+
}
|
|
60
65
|
class SecurityFilterPlugin {
|
|
61
66
|
constructor(config, options) {
|
|
62
67
|
this.config = config || {};
|
|
@@ -233,13 +238,7 @@ class SecurityFilterPlugin {
|
|
|
233
238
|
if (this.config.dryRun) {
|
|
234
239
|
return next();
|
|
235
240
|
}
|
|
236
|
-
res.status(403).json({
|
|
237
|
-
error: 'Package blocked by security filter',
|
|
238
|
-
package: packageName,
|
|
239
|
-
version: version || 'all versions',
|
|
240
|
-
reason: blockResult.reason,
|
|
241
|
-
timestamp: new Date().toISOString()
|
|
242
|
-
});
|
|
241
|
+
res.status(403).json(forbiddenBody(packageName, blockResult.reason, { version: version || 'all versions' }));
|
|
243
242
|
return;
|
|
244
243
|
}
|
|
245
244
|
this.logger.debug(`[Middleware] [OK] ALLOWED TARBALL: ${packageName}@${version || 'unknown'}`);
|
|
@@ -252,10 +251,7 @@ class SecurityFilterPlugin {
|
|
|
252
251
|
if (this.config.dryRun) {
|
|
253
252
|
return next();
|
|
254
253
|
}
|
|
255
|
-
|
|
256
|
-
name: packageName,
|
|
257
|
-
versions: {},
|
|
258
|
-
'dist-tags': {},
|
|
254
|
+
res.status(403).json(forbiddenBody(packageName, blockResult.reason, {
|
|
259
255
|
security: {
|
|
260
256
|
blocked: true,
|
|
261
257
|
reason: blockResult.reason,
|
|
@@ -266,21 +262,24 @@ class SecurityFilterPlugin {
|
|
|
266
262
|
},
|
|
267
263
|
blockedAt: new Date().toISOString(),
|
|
268
264
|
rules: this._getAppliedRules(packageName),
|
|
269
|
-
message: `This package has been blocked by security filter: ${blockResult.reason}`,
|
|
270
265
|
contact: 'Please contact your registry administrator for more information',
|
|
271
266
|
},
|
|
272
|
-
|
|
273
|
-
blocked: true,
|
|
274
|
-
reason: blockResult.reason,
|
|
275
|
-
blockedBy: 'verdaccio-security-filter',
|
|
276
|
-
blockedAt: new Date().toISOString(),
|
|
277
|
-
}
|
|
278
|
-
};
|
|
279
|
-
res.status(200).json(blockedResponse);
|
|
267
|
+
}));
|
|
280
268
|
return;
|
|
281
269
|
}
|
|
282
|
-
|
|
283
|
-
|
|
270
|
+
this.logger.debug(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
|
|
271
|
+
if (!this.config.dryRun) {
|
|
272
|
+
const startedAt = Date.now();
|
|
273
|
+
const send = res.send.bind(res);
|
|
274
|
+
res.send = (body) => {
|
|
275
|
+
const block = filterBlocks.get(packageName);
|
|
276
|
+
if (block && block.at >= startedAt && res.statusCode < 300) {
|
|
277
|
+
res.removeHeader('ETag');
|
|
278
|
+
res.status(403);
|
|
279
|
+
return send(JSON.stringify(forbiddenBody(packageName, block.security.reason, { security: block.security })));
|
|
280
|
+
}
|
|
281
|
+
return send(body);
|
|
282
|
+
};
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
next();
|
|
@@ -334,7 +333,14 @@ class SecurityFilterPlugin {
|
|
|
334
333
|
filter_metadata(packageInfo) {
|
|
335
334
|
return __awaiter(this, void 0, void 0, function* () {
|
|
336
335
|
const filtered = yield this._filterMetadata(packageInfo);
|
|
337
|
-
if (!this.config.dryRun
|
|
336
|
+
if (!this.config.dryRun) {
|
|
337
|
+
const security = filtered.security;
|
|
338
|
+
if (security === null || security === void 0 ? void 0 : security.blocked) {
|
|
339
|
+
filterBlocks.set(packageInfo.name, { security, at: Date.now() });
|
|
340
|
+
}
|
|
341
|
+
return filtered;
|
|
342
|
+
}
|
|
343
|
+
if (filtered === packageInfo) {
|
|
338
344
|
return filtered;
|
|
339
345
|
}
|
|
340
346
|
const total = Object.keys(packageInfo.versions || {}).length;
|
package/package.json
CHANGED