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.
Files changed (3) hide show
  1. package/README.md +27 -30
  2. package/build/index.js +29 -23
  3. 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
- This plugin implements **two layers of protection** to block malicious packages:
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
- When a client requests package information (e.g., `npm info lodash`), the plugin:
157
- 1. Checks if the package is allowed (whitelist mode)
158
- 2. Validates against blocked patterns, scopes, and age requirements
159
- 3. **Throws HTTP 404 error** for completely blocked packages (patterns, scopes, whitelist violations)
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
- **Result**: Blocked packages return "404 Not Found" - as if they don't exist in the registry.
161
+ npm then resolves ranges against the versions that are left.
163
162
 
164
- ### Layer 2: Middleware Interception (`register_middlewares`)
165
- When a client tries to download a tarball file (e.g., during `npm install`), the plugin:
166
- 1. Intercepts **all HTTP requests** early in the middleware chain
167
- 2. Extracts package name and version from the request URL
168
- 3. Applies all security checks (whitelist, patterns, scopes, blocked versions, range rules)
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
- **Result**: Even if metadata is cached, tarball downloads are blocked at the HTTP level.
169
+ ### What a client sees
172
170
 
173
- ### Why Both Layers?
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
- → GET /hawk (metadata request)
183
- → [Security Filter] Blocked hawk@* - Package is not in whitelist
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
- const blockedResponse = {
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
- _security: {
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
- else {
283
- this.logger.debug(`[Middleware] [OK] ALLOWED METADATA: ${packageName}`);
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 || filtered === packageInfo) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verdaccio-security-filter",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Advanced security filter plugin for Verdaccio with version range blocking and fallback strategies",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",