backend-manager 5.0.186 → 5.0.188

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/CLAUDE.md CHANGED
@@ -207,6 +207,58 @@ require(functionsDir + '/node_modules/backend-manager')
207
207
  ### Prefer fs-jetpack
208
208
  Use `fs-jetpack` over `fs` or `fs-extra` for file operations.
209
209
 
210
+ ## Sanitization (XSS Prevention)
211
+
212
+ BEM automatically sanitizes all incoming request data — stripping HTML tags and trimming whitespace from every string field. This happens in the middleware pipeline before route handlers execute, so **routes receive clean data by default**.
213
+
214
+ ### How It Works
215
+ 1. **Schema fields**: Sanitized per-field during the middleware pipeline. Fields can opt out with `sanitize: false` in the schema.
216
+ 2. **Non-schema fields** (when `setupSettings: false` or `includeNonSchemaSettings: true`): All strings are sanitized with no opt-out.
217
+ 3. The middleware uses `Manager.Utilities().sanitize()` under the hood.
218
+
219
+ ### Schema Opt-Out
220
+ For fields that legitimately need HTML (rich text, email templates, etc.), set `sanitize: false` in the schema:
221
+ ```javascript
222
+ // This field will NOT be sanitized — raw HTML is preserved
223
+ htmlContent: {
224
+ types: ['string'],
225
+ default: '',
226
+ sanitize: false,
227
+ },
228
+ // This field IS sanitized (default behavior, no flag needed)
229
+ name: {
230
+ types: ['string'],
231
+ default: '',
232
+ },
233
+ ```
234
+
235
+ ### Route-Level Opt-Out
236
+ Disable sanitization entirely for a route (rare — only for routes that handle raw HTML everywhere):
237
+ ```javascript
238
+ // In functions/index.js
239
+ Manager.Middleware(req, res).run('my-route', { sanitize: false });
240
+ ```
241
+
242
+ ### Manual Sanitization (Outside Middleware)
243
+ For cron jobs, event handlers, or anywhere outside the request pipeline, use `utilities.sanitize()` directly:
244
+ ```javascript
245
+ // Available in route context
246
+ const clean = utilities.sanitize(untrustedData);
247
+
248
+ // Or via Manager
249
+ const clean = Manager.Utilities().sanitize(untrustedData);
250
+ ```
251
+ Accepts any data type — strings, objects, arrays, primitives. Walks objects/arrays recursively, strips HTML from strings, passes everything else through unchanged.
252
+
253
+ ### Route Handler Context
254
+ The middleware injects these into every route handler:
255
+ ```javascript
256
+ module.exports = async ({ Manager, assistant, analytics, usage, user, settings, libraries, utilities }) => {
257
+ // settings — already sanitized by middleware
258
+ // utilities — Manager.Utilities() instance for manual sanitization
259
+ };
260
+ ```
261
+
210
262
  ## Creating New Components
211
263
 
212
264
  ### New API Command
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backend-manager",
3
- "version": "5.0.186",
3
+ "version": "5.0.188",
4
4
  "description": "Quick tools for developing Firebase functions",
5
5
  "main": "src/manager/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "start": "node src/manager/index.js",
14
+ "prepublishOnly": "node scripts/update-disposable-domains.js",
14
15
  "prepare": "node -e \"require('prepare-package')()\"",
15
16
  "prepare:watch": "node -e \"require('prepare-package/watch')()\""
16
17
  },
@@ -78,6 +79,7 @@
78
79
  "npm-api": "^1.0.1",
79
80
  "paypal-server-api": "^2.0.14",
80
81
  "pushid": "^1.0.0",
82
+ "sanitize-html": "^2.17.2",
81
83
  "shortid": "^2.2.17",
82
84
  "stripe": "^20.3.1",
83
85
  "uid-generator": "^2.0.0",
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Fetches the latest disposable email domain list from GitHub and saves it as JSON.
5
+ *
6
+ * Source: https://github.com/disposable-email-domains/disposable-email-domains
7
+ * (curated, ~5k domains, low false-positive rate)
8
+ *
9
+ * Run manually: node scripts/update-disposable-domains.js
10
+ * Runs automatically on: npm prepublishOnly
11
+ */
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const SOURCE_URL = 'https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/main/disposable_email_blocklist.conf';
16
+ const OUTPUT_PATH = path.join(__dirname, '..', 'src', 'manager', 'libraries', 'disposable-domains.json');
17
+
18
+ async function main() {
19
+ console.log('Fetching disposable domain list...');
20
+
21
+ const response = await fetch(SOURCE_URL);
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
25
+ }
26
+
27
+ const text = await response.text();
28
+ const domains = text
29
+ .trim()
30
+ .split('\n')
31
+ .map(d => d.trim().toLowerCase())
32
+ .filter(Boolean);
33
+
34
+ const unique = [...new Set(domains)].sort();
35
+
36
+ fs.writeFileSync(OUTPUT_PATH, JSON.stringify(unique, null, 2) + '\n');
37
+
38
+ console.log(`Updated disposable-domains.json: ${unique.length} domains`);
39
+ }
40
+
41
+ main().catch((e) => {
42
+ console.warn('Warning: Failed to update disposable domains:', e.message);
43
+ console.warn('Using existing list. Run manually later: node scripts/update-disposable-domains.js');
44
+ });
@@ -1,10 +1,12 @@
1
- const ERROR_TOO_MANY_ATTEMPTS = 'You have created too many accounts with our service. Please try again later.';
1
+ const { isDisposable } = require('../../libraries/email/validation.js');
2
+
3
+ const ERROR_TOO_MANY_ATTEMPTS = 'Unable to create account at this time. Please try again later.';
4
+ const ERROR_DISPOSABLE_EMAIL = 'This email domain is not allowed. Please use a different email address.';
2
5
  const MAX_SIGNUPS_PER_DAY = 2;
3
6
 
4
7
  /**
5
- * beforeUserCreated - IP Rate Limiting ONLY
8
+ * beforeUserCreated - Disposable email blocking + IP rate limiting
6
9
  *
7
- * This function ONLY handles IP rate limiting to prevent abuse.
8
10
  * User doc creation is handled by on-create.js (which fires for all user creations including Admin SDK).
9
11
  *
10
12
  * Why not create user doc here?
@@ -16,7 +18,14 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
16
18
  const { functions } = libraries;
17
19
  const ipAddress = context.ipAddress || '';
18
20
 
19
- assistant.log(`beforeCreate: ${user.uid}`, { email: user.email, ip: ipAddress });
21
+ assistant.log(`beforeCreate: ${user.uid} (${user.email})`, user, context);
22
+
23
+ // Block disposable email domains
24
+ if (isDisposable(user.email)) {
25
+ assistant.error(`beforeCreate: Blocked disposable email ${user.email}`);
26
+
27
+ throw new functions.auth.HttpsError('invalid-argument', ERROR_DISPOSABLE_EMAIL);
28
+ }
20
29
 
21
30
  // Skip rate limiting if no IP (shouldn't happen in production)
22
31
  if (!ipAddress) {
@@ -11,7 +11,7 @@ module.exports = async ({ Manager, assistant, user, context, libraries }) => {
11
11
  const startTime = Date.now();
12
12
  const { admin } = libraries;
13
13
 
14
- assistant.log(`beforeSignIn: ${user.uid}`, { email: user.email, ip: context.ipAddress });
14
+ assistant.log(`beforeSignIn: ${user.uid} (${user.email})`, user, context);
15
15
 
16
16
  const now = new Date();
17
17
 
@@ -35,7 +35,7 @@ Middleware.prototype.run = function (libPath, options) {
35
35
  options.setupAnalytics = typeof options.setupAnalytics === 'boolean' ? options.setupAnalytics : true;
36
36
  options.setupUsage = typeof options.setupUsage === 'boolean' ? options.setupUsage : true;
37
37
  options.setupSettings = typeof options.setupSettings === 'undefined' ? true : options.setupSettings;
38
- options.cleanSettings = typeof options.cleanSettings === 'undefined' ? true : options.cleanSettings;
38
+ options.sanitize = typeof options.sanitize === 'undefined' ? true : options.sanitize;
39
39
  options.includeNonSchemaSettings = typeof options.includeNonSchemaSettings === 'undefined' ? false : options.includeNonSchemaSettings;
40
40
  options.schema = typeof options.schema === 'undefined' ? libPath : options.schema;
41
41
  options.parseMultipartFormData = typeof options.parseMultipartFormData === 'undefined' ? true : options.parseMultipartFormData;
@@ -168,14 +168,6 @@ Middleware.prototype.run = function (libPath, options) {
168
168
  assistant.settings = merge(data, assistant.settings)
169
169
  }
170
170
 
171
- // Clean settings by looping through and trimming all strings
172
- if (options.cleanSettings) {
173
- clean(assistant.settings);
174
- }
175
-
176
- // Log
177
- assistant.log(`Middleware.process(): Resolved settings with schema=${options.schema}`, safeStringify(assistant.settings));
178
-
179
171
  // Log multipart files if they exist
180
172
  const files = assistant.request.multipartData.files || {};
181
173
  if (files) {
@@ -185,6 +177,19 @@ Middleware.prototype.run = function (libPath, options) {
185
177
  assistant.settings = data;
186
178
  }
187
179
 
180
+ // Sanitize settings — trim whitespace and strip HTML from all strings
181
+ // Respects sanitize: false on individual schema fields
182
+ if (options.sanitize) {
183
+ const schema = options.setupSettings ? Manager.Settings().schema : null;
184
+ const utilities = Manager.Utilities();
185
+
186
+ // Walk settings, skipping fields the schema marks as sanitize: false
187
+ assistant.settings = sanitizeWithSchema(utilities, assistant.settings, schema);
188
+ }
189
+
190
+ // Log
191
+ assistant.log(`Middleware.process(): Resolved settings with schema=${options.schema}`, safeStringify(assistant.settings));
192
+
188
193
  // Build context object for route handler
189
194
  const context = {
190
195
  Manager: Manager,
@@ -194,6 +199,7 @@ Middleware.prototype.run = function (libPath, options) {
194
199
  settings: assistant.settings,
195
200
  analytics: assistant.analytics,
196
201
  libraries: Manager.libraries,
202
+ utilities: Manager.Utilities(),
197
203
  };
198
204
 
199
205
  // Execute route handler
@@ -208,15 +214,29 @@ Middleware.prototype.run = function (libPath, options) {
208
214
  });
209
215
  };
210
216
 
211
- function clean(obj) {
212
- for (let key in obj) {
213
- if (typeof obj[key] === 'object') {
214
- clean(obj[key]);
215
- } else if (typeof obj[key] === 'string') {
216
- obj[key] = obj[key]
217
- .trim();
217
+ function sanitizeWithSchema(utilities, obj, schema) {
218
+ // Not an object sanitize directly
219
+ if (obj == null || typeof obj !== 'object') {
220
+ return utilities.sanitize(obj);
221
+ }
222
+
223
+ // Arrays — sanitize each item (no schema for array items)
224
+ if (Array.isArray(obj)) {
225
+ return obj.map(item => utilities.sanitize(item));
226
+ }
227
+
228
+ // Objects — walk keys, skip fields where schema says sanitize: false
229
+ const result = {};
230
+ for (const [key, value] of Object.entries(obj)) {
231
+ const schemaNode = schema ? schema[key] : null;
232
+
233
+ if (schemaNode && schemaNode.sanitize === false) {
234
+ result[key] = value;
235
+ } else {
236
+ result[key] = sanitizeWithSchema(utilities, value, schemaNode);
218
237
  }
219
238
  }
239
+ return result;
220
240
  }
221
241
 
222
242
  function stripUrl(url) {
@@ -8,6 +8,7 @@ const powertools = require('node-powertools');
8
8
  const _ = require('lodash');
9
9
  const moment = require('moment');
10
10
 
11
+
11
12
  function Settings(m) {
12
13
  const self = this;
13
14
 
@@ -129,6 +130,7 @@ Settings.prototype.resolve = function (assistant, schema, settings, options) {
129
130
  available: typeof schemaNode.available === 'undefined' ? true : schemaNode.available,
130
131
  min: typeof schemaNode.min === 'undefined' ? undefined : schemaNode.min,
131
132
  max: typeof schemaNode.max === 'undefined' ? undefined : schemaNode.max,
133
+ sanitize: typeof schemaNode.sanitize === 'undefined' ? true : schemaNode.sanitize,
132
134
  }
133
135
 
134
136
  // Update schema
@@ -1,5 +1,6 @@
1
1
  let nanoId;
2
2
  let _;
3
+ let sanitizeHtml;
3
4
 
4
5
  function Utilities(Manager) {
5
6
  const self = this;
@@ -467,4 +468,43 @@ Utilities.prototype.get = function (docPath, options) {
467
468
  });
468
469
  };
469
470
 
471
+ /**
472
+ * Sanitize input by stripping HTML tags and trimming strings.
473
+ * Accepts any data type — walks objects/arrays recursively.
474
+ *
475
+ * @param {*} input - The data to sanitize (string, object, array, or primitive)
476
+ * @returns {*} Sanitized copy (objects/arrays) or sanitized value (strings)
477
+ */
478
+ Utilities.prototype.sanitize = function (input) {
479
+ // Handle null/undefined
480
+ if (input == null) {
481
+ return input;
482
+ }
483
+
484
+ // Handle strings
485
+ if (typeof input === 'string') {
486
+ sanitizeHtml = sanitizeHtml
487
+ ? sanitizeHtml
488
+ : require('sanitize-html');
489
+ return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} }).trim();
490
+ }
491
+
492
+ // Handle arrays
493
+ if (Array.isArray(input)) {
494
+ return input.map(item => this.sanitize(item));
495
+ }
496
+
497
+ // Handle objects
498
+ if (typeof input === 'object') {
499
+ const result = {};
500
+ for (const [key, value] of Object.entries(input)) {
501
+ result[key] = this.sanitize(value);
502
+ }
503
+ return result;
504
+ }
505
+
506
+ // Numbers, booleans, etc. — pass through
507
+ return input;
508
+ };
509
+
470
510
  module.exports = Utilities;
@@ -0,0 +1,12 @@
1
+ [
2
+ "10mail.info",
3
+ "mail2me.co",
4
+ "maximail.fyi",
5
+ "maximail.vip",
6
+ "mimimail.me",
7
+ "minitts.net",
8
+ "pickmail.org",
9
+ "pickmemail.com",
10
+ "picomail.biz",
11
+ "sharebot.net"
12
+ ]