@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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
*
|
|
305
|
-
*
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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:
|
|
566
|
+
// CHECK 4b: All policy clauses must match whitelisted patterns only
|
|
479
567
|
for (const p of policies) {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|