@soulbatical/tetra-dev-toolkit 1.17.1 → 1.17.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.
@@ -281,32 +281,120 @@ function isWideOpen(using) {
281
281
  }
282
282
 
283
283
  /**
284
- * Forbidden patterns in RLS policies — these bypass RLS and defeat the purpose.
285
- * Valid patterns (from sparkbuddy-live):
286
- * - organization_id IN (SELECT auth_admin_organizations())
287
- * - user_id = auth.uid()
288
- * - USING(true) for public tables only
289
- * - Subquery to parent table with org/user check
284
+ * Whitelist-based RLS policy validation.
290
285
  *
291
- * Everything else that grants blanket access is a security hole.
286
+ * ONLY these building blocks are allowed in USING/WITH CHECK clauses.
287
+ * Derived from sparkbuddy-live (the reference implementation).
288
+ * Everything that doesn't match is rejected as unrecognized.
289
+ *
290
+ * To add a new pattern: add it here AND document why it's safe.
292
291
  */
293
- const FORBIDDEN_RLS_PATTERNS = [
294
- { pattern: /service_role/i, label: 'service_role bypass' },
295
- { pattern: /current_setting\s*\(\s*'role'/i, label: 'PostgreSQL role check bypass' },
296
- { pattern: /current_setting\s*\(\s*'request\.jwt\.claims'/i, label: 'JWT claims role bypass' },
297
- { pattern: /session_user\s*=\s*'postgres'/i, label: 'session_user postgres bypass' },
298
- { pattern: /current_user\s*=\s*'postgres'/i, label: 'current_user postgres bypass' },
299
- { pattern: /auth\.role\s*\(\)\s*=\s*'service_role'/i, label: 'auth.role() service_role bypass' },
300
- { pattern: /pg_has_role/i, label: 'pg_has_role bypass' },
292
+ const ALLOWED_RLS_ATOMS = [
293
+ // Org isolation via helper functions
294
+ /auth_admin_organizations\s*\(\)/i,
295
+ /auth_user_organizations\s*\(\)/i,
296
+ /auth_org_id\s*\(\)/i,
297
+ /auth_current_user_id\s*\(\)/i,
298
+ // User isolation
299
+ /auth\.uid\s*\(\)/i,
300
+ // Column comparisons (org_id, user_id, etc.)
301
+ /organization_id/i,
302
+ /organizationid/i,
303
+ /active_organization_id/i,
304
+ /user_id/i,
305
+ /userid/i,
306
+ /owner_id/i,
307
+ /created_by/i,
308
+ /creator_id/i,
309
+ /shared_by/i,
310
+ /sparkbuddy_user_id/i,
311
+ /user_public_id/i,
312
+ // Boolean/status column checks (for public/active filtering)
313
+ /is_active/i,
314
+ /is_public/i,
315
+ /is_template/i,
316
+ /is_published/i,
317
+ /anonymous_access/i,
318
+ /allow_cross_org_usage/i,
319
+ /visibility_level/i,
320
+ /published_status/i,
321
+ /post_type/i,
322
+ /status/i,
323
+ /active/i,
324
+ /invitation_token/i,
325
+ /original_testimonial_id/i,
326
+ /expires_at/i,
327
+ /session_id/i,
328
+ // Subqueries to parent tables
329
+ /\bIN\s*\(\s*SELECT\b/i,
330
+ /\bEXISTS\s*\(\s*SELECT\b/i,
331
+ // Literals and operators
332
+ /true/i,
333
+ /false/i,
334
+ /null/i,
335
+ /now\s*\(\)/i,
336
+ /\bAND\b/i,
337
+ /\bOR\b/i,
338
+ /\bNOT\b/i,
339
+ /\bIS\b/i,
340
+ /\bANY\b/i,
341
+ /\bARRAY\b/i,
342
+ // Legacy JWT pattern (sparkbuddy initial schema)
343
+ /auth\.jwt\s*\(\)/i,
344
+ /app_metadata/i,
345
+ // current_setting for app context (NOT role/jwt.claims bypass)
346
+ /current_setting\s*\(\s*'app\./i,
301
347
  ]
302
348
 
303
349
  /**
304
- * Check if a USING/WITH CHECK clause contains forbidden bypass patterns
305
- * Returns array of { label } for each forbidden pattern found
350
+ * Patterns that are NEVER allowed in RLS policies, regardless of context.
351
+ * These bypass RLS and defeat the purpose of having policies at all.
306
352
  */
307
- function findForbiddenPatterns(clause) {
308
- if (!clause) return []
309
- return FORBIDDEN_RLS_PATTERNS.filter(({ pattern }) => pattern.test(clause)).map(({ label }) => label)
353
+ const BANNED_RLS_PATTERNS = [
354
+ { pattern: /service_role/i, label: 'service_role bypass — service role already bypasses RLS at the Supabase layer' },
355
+ { pattern: /current_setting\s*\(\s*'role'\s*(?:,\s*true\s*)?\)/i, label: 'PostgreSQL role check — bypasses tenant isolation' },
356
+ { pattern: /current_setting\s*\(\s*'request\.jwt\.claims'/i, label: 'JWT claims role check — bypasses tenant isolation' },
357
+ { pattern: /session_user/i, label: 'session_user check — bypasses tenant isolation' },
358
+ { pattern: /current_user\s*=/i, label: 'current_user check — bypasses tenant isolation' },
359
+ { pattern: /auth\.role\s*\(\)/i, label: 'auth.role() check — bypasses tenant isolation' },
360
+ { pattern: /pg_has_role/i, label: 'pg_has_role — bypasses tenant isolation' },
361
+ ]
362
+
363
+ /**
364
+ * Validate an RLS clause against the whitelist.
365
+ * Returns null if valid, or a description of what's wrong.
366
+ */
367
+ function validateRlsClause(clause) {
368
+ if (!clause || !clause.trim()) return null
369
+
370
+ // First check for explicitly banned patterns
371
+ for (const { pattern, label } of BANNED_RLS_PATTERNS) {
372
+ if (pattern.test(clause)) return label
373
+ }
374
+
375
+ // Strip known-safe tokens and see if anything suspicious remains
376
+ let stripped = clause
377
+ // Remove string literals
378
+ stripped = stripped.replace(/'[^']*'/g, '')
379
+ // Remove numbers
380
+ stripped = stripped.replace(/\b\d+\b/g, '')
381
+ // Remove known-safe identifiers and functions
382
+ for (const atom of ALLOWED_RLS_ATOMS) {
383
+ stripped = stripped.replace(new RegExp(atom.source, 'gi'), '')
384
+ }
385
+ // Remove SQL syntax noise (parens, commas, operators, quotes, casts, aliases)
386
+ stripped = stripped.replace(/[(),"=<>!:.|{}\-\+\*\/\s]/g, '')
387
+ stripped = stripped.replace(/\b(AS|FROM|WHERE|SELECT|JOIN|ON|LIMIT|uuid|text|boolean|integer|jsonb|json|public|ARRAY|FOR)\b/gi, '')
388
+ // Remove table/column qualifiers that look like identifiers (lowercase + underscore)
389
+ stripped = stripped.replace(/\b[a-z_][a-z0-9_]*\b/gi, '')
390
+ stripped = stripped.trim()
391
+
392
+ // If anything substantial remains, it's an unrecognized pattern
393
+ if (stripped.length > 0) {
394
+ return `Unrecognized RLS clause content: "${clause.substring(0, 120)}". Only whitelisted patterns are allowed.`
395
+ }
396
+
397
+ return null
310
398
  }
311
399
 
312
400
  export async function run(config, projectRoot) {
@@ -475,24 +563,24 @@ export async function run(config, projectRoot) {
475
563
  }
476
564
  }
477
565
 
478
- // CHECK 4b: No forbidden bypass patterns in any policy clause
566
+ // CHECK 4b: All policy clauses must match whitelisted patterns only
479
567
  for (const p of policies) {
480
- const usingViolations = findForbiddenPatterns(p.using)
481
- const checkViolations = findForbiddenPatterns(p.withCheck)
482
- const allViolations = [...new Set([...usingViolations, ...checkViolations])]
483
-
484
- if (allViolations.length > 0) {
485
- results.passed = false
486
- results.findings.push({
487
- file: p.file,
488
- line: 1,
489
- type: 'rls-bypass-pattern',
490
- severity: 'critical',
491
- message: `Policy "${p.name}" on table "${tableName}" contains forbidden bypass pattern(s): ${allViolations.join(', ')}. RLS policies must ONLY use auth.uid(), auth_admin_organizations(), or parent-table subqueries. Service role bypasses RLS automatically adding it to policies defeats the purpose.`,
492
- fix: `Remove the bypass clause. Valid patterns: USING (organization_id IN (SELECT auth_admin_organizations())) or USING (user_id = auth.uid()).`
493
- })
494
- results.summary.critical++
495
- results.summary.total++
568
+ for (const [clauseType, clause] of [['USING', p.using], ['WITH CHECK', p.withCheck]]) {
569
+ if (!clause) continue
570
+ const violation = validateRlsClause(clause)
571
+ if (violation) {
572
+ results.passed = false
573
+ results.findings.push({
574
+ file: p.file,
575
+ line: 1,
576
+ type: 'rls-invalid-clause',
577
+ severity: 'critical',
578
+ message: `Policy "${p.name}" on table "${tableName}" has invalid ${clauseType} clause: ${violation}`,
579
+ fix: `Only whitelisted patterns are allowed. Valid: auth_admin_organizations(), auth.uid(), org/user column checks, parent-table subqueries, boolean column filters. See ALLOWED_RLS_ATOMS in config-rls-alignment.js.`
580
+ })
581
+ results.summary.critical++
582
+ results.summary.total++
583
+ }
496
584
  }
497
585
  }
498
586
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulbatical/tetra-dev-toolkit",
3
- "version": "1.17.1",
3
+ "version": "1.17.2",
4
4
  "publishConfig": {
5
5
  "access": "restricted"
6
6
  },