verdaccio-security-filter 1.4.0 → 1.4.2
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 +29 -32
- package/build/index.js +34 -28
- package/build/lib/license-checker.d.ts +1 -0
- package/build/lib/license-checker.js +5 -1
- 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)
|
|
@@ -451,9 +448,9 @@ Recommended before turning on `cveCheck.autoBlock`, a license policy or the auth
|
|
|
451
448
|
|--------|------|---------|-------------|
|
|
452
449
|
| `licenses.allowed` | `string[]` | `[]` | Allowed license list |
|
|
453
450
|
| `licenses.blocked` | `string[]` | `[]` | Blocked license list |
|
|
454
|
-
| `licenses.requireLicense` | `boolean` | `true` | Reject versions without a license field |
|
|
451
|
+
| `licenses.requireLicense` | `boolean` | `true` with `allowed`, otherwise `false` | Reject versions without a license field |
|
|
455
452
|
|
|
456
|
-
The license check runs when `licenses.allowed` is not empty. Every version is checked: versions with a rejected license are removed from the metadata, and the package is blocked as a whole only when no version is left.
|
|
453
|
+
The license check runs when `licenses.allowed` or `licenses.blocked` is not empty, or `licenses.requireLicense` is `true`. Every version is checked: versions with a rejected license are removed from the metadata, and the package is blocked as a whole only when no version is left.
|
|
457
454
|
|
|
458
455
|
### Whitelist Options
|
|
459
456
|
|
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 || {};
|
|
@@ -141,7 +146,7 @@ class SecurityFilterPlugin {
|
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
_getEnabledFeatures() {
|
|
144
|
-
var _a, _b, _c, _d
|
|
149
|
+
var _a, _b, _c, _d;
|
|
145
150
|
const features = [];
|
|
146
151
|
if (this.config.dryRun)
|
|
147
152
|
features.push('dry-run');
|
|
@@ -151,11 +156,11 @@ class SecurityFilterPlugin {
|
|
|
151
156
|
features.push('metrics');
|
|
152
157
|
if ((_b = this.config.cveCheck) === null || _b === void 0 ? void 0 : _b.enabled)
|
|
153
158
|
features.push('cve-checking');
|
|
154
|
-
if (
|
|
159
|
+
if (this.licenseChecker.enabled)
|
|
155
160
|
features.push('license-filtering');
|
|
156
|
-
if ((
|
|
161
|
+
if ((_c = this.config.packageAge) === null || _c === void 0 ? void 0 : _c.enabled)
|
|
157
162
|
features.push('package-age-verification');
|
|
158
|
-
if ((
|
|
163
|
+
if ((_d = this.config.authorFilter) === null || _d === void 0 ? void 0 : _d.enabled)
|
|
159
164
|
features.push('author-filtering');
|
|
160
165
|
if (this.securityRules.versionRangeRules.length > 0)
|
|
161
166
|
features.push('version-range-rules');
|
|
@@ -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;
|
|
@@ -397,7 +403,7 @@ class SecurityFilterPlugin {
|
|
|
397
403
|
pkg = this._removeVersions(pkg, vulnerableVersions);
|
|
398
404
|
}
|
|
399
405
|
}
|
|
400
|
-
if (this.
|
|
406
|
+
if (this.licenseChecker.enabled) {
|
|
401
407
|
const rejected = this._rejectedVersions(pkg, data => this.licenseChecker.checkLicense(data));
|
|
402
408
|
if (rejected.all) {
|
|
403
409
|
const reason = rejected.first.reason || 'License not allowed';
|
|
@@ -3,12 +3,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LicenseChecker = void 0;
|
|
4
4
|
class LicenseChecker {
|
|
5
5
|
constructor(config) {
|
|
6
|
+
var _a;
|
|
6
7
|
this.config = {
|
|
7
8
|
allowed: (config === null || config === void 0 ? void 0 : config.allowed) || [],
|
|
8
9
|
blocked: (config === null || config === void 0 ? void 0 : config.blocked) || [],
|
|
9
|
-
requireLicense: (config === null || config === void 0 ? void 0 : config.requireLicense) !==
|
|
10
|
+
requireLicense: (_a = config === null || config === void 0 ? void 0 : config.requireLicense) !== null && _a !== void 0 ? _a : ((config === null || config === void 0 ? void 0 : config.allowed) || []).length > 0,
|
|
10
11
|
};
|
|
11
12
|
}
|
|
13
|
+
get enabled() {
|
|
14
|
+
return this.config.allowed.length > 0 || this.config.blocked.length > 0 || this.config.requireLicense;
|
|
15
|
+
}
|
|
12
16
|
checkLicense(versionData) {
|
|
13
17
|
const license = this.extractLicense(versionData);
|
|
14
18
|
if (!license || license === 'UNLICENSED' || license === '') {
|
package/package.json
CHANGED