QuackScan Security
Security Assessment

Security Assessment Report

acme-saas.app

Comprehensive Security Audit

Overall Risk Rating CRITICAL
Audit Date February 2, 2026
Findings 17
5 Critical 5 High 4 Medium 2 Low 1 Info

Table of Contents

  1. Executive Summary
  2. The Big Picture
  3. Risk Overview
  4. Detailed Findings (17 vulnerabilities)
  5. Compliance Mapping
  6. Remediation Roadmap
  7. Appendix

1. Executive Summary

5 Critical
5 High
4 Medium
2 Low
Immediate action required.

This assessment of acme-saas.app found 17 vulnerabilities (5 critical, 5 high, 4 medium, 2 low). Overall risk rating: CRITICAL.

Key Findings

CRITICAL
CATASTROPHIC: 9 Additional Tables Publicly Readable - 2.4M+ Records Exposed
Anyone on the internet can read 4.8M+ records from your database without logging in.
CRITICAL DATA BREACH RISK. The entire application database is effectively public. Most severe exposures: (1) profiles table: 119K user records with full_name, avatar_url, is_admin flag, stripe_customer_id, stripe_subscription_id, subscription_status, is_banned, is_tester flags, daily/monthly usage counters. (2) messages table: 1.6M+ AI chat messages containing full user conversations with the AI including generated HTML/code - this is private user content. (3) chats table: 320K chat sessions with user_id linkage. (4) file_attachments: 276K files with direct download URLs to user-uploaded content. (5) shared_code: 15K templates with full HTML source code, user_id, custom_domain info, DNS verification status. An attacker can trivially: enumerate all users, identify admins (is_admin=true), read all private conversations, download all user files, and map the entire user base with their subscription status.
CRITICAL
119,709 User Profiles Exposed Including Admin Status and Stripe IDs
User names, emails, payment details, profile photos is exposed to anyone who knows where to look.
Complete user directory with PII (names, photos, locations), admin enumeration (is_admin field), payment information (Stripe customer/subscription IDs allow lookup in Stripe), subscription intelligence (who pays, what tier, when they cancel and why), and behavioral data (usage counters). GDPR Article 5 violation - personal data must be processed lawfully with appropriate security.
CRITICAL
1.6M+ Private AI Chat Messages Readable by Anyone
1.6M+ records containing private messages are publicly accessible.
Every conversation every user has ever had with the platform's AI is publicly readable. This includes: proprietary designs and code users asked the AI to generate, potentially confidential business information shared in prompts, user IP addresses (ip column), and the full creative output. This is equivalent to making every user's private workspace public. Massive GDPR, privacy, and intellectual property liability.
CRITICAL
Figma Personal Access Token Hardcoded in Client Bundle
A secret key for Figma is embedded in the public JavaScript. Anyone can extract it and use it.
CRITICAL - A Figma personal access token (figd_ prefix) is hardcoded in the client bundle as a fallback when no user-provided token exists. Anyone can extract this token and use it to: (1) Read all files in the associated Figma account, (2) Access design assets, (3) Potentially modify files depending on token scope, (4) Access team/org data. This is a direct credential exposure granting authenticated API access to a third-party service.
CRITICAL
Unauthenticated Access to blocked_ips Table Exposes IP Addresses
67 records of sensitive data can be read without authorization.
IP addresses are considered PII under GDPR (EU regulation). The blocked_ips table leaks: (1) IP addresses of blocked users - potentially identifying individuals, (2) UUIDs of admins who performed the blocking via blocked_by field, (3) Timing information of moderation actions. An attacker can enumerate all blocked IPs and correlate admin UUIDs across tables.

Immediate Actions Required

PriorityTimeframeAction
P1 Immediate EMERGENCY: Enable RLS on ALL tables immediately. As an interim measure, consider enabling RLS with a deny-all policy on the most sensitive tables (pro...
P2 Immediate Enable RLS on profiles immediately. Public profile fields (name, avatar, slug) should have a separate public-facing view. Sensitive fields (is_admin, ...
P3 Immediate RLS policy: messages should only be readable by the chat owner. CREATE POLICY messages_owner_only ON messages FOR SELECT USING (chat_id IN (SELECT id ...
P4 Immediate IMMEDIATELY rotate this Figma API token. Remove it from the client bundle entirely. Implement Figma API access through a server-side edge function tha...
P5 Immediate Implement RLS policy: CREATE POLICY blocked_ips_admin_only ON blocked_ips FOR SELECT USING (auth.uid() IN (SELECT id FROM profiles WHERE role = 'admin...

The Big Picture

Here's the bottom line: acme-saas.app has serious security problems that need fixing right now. We found issues in 5 areas, including 5 critical-severity findings. The sections below break down what's wrong in plain language.

Your database is wide open

5 findings

9 database tables have no access controls. That means 5.2M+ records are readable by anyone on the internet — no login required.

  • CRITICAL CATASTROPHIC: 9 Additional Tables Publicly Readable - 2.4M+ Records Exposed
  • HIGH 276,423 User-Uploaded Files Accessible via Direct URLs
  • HIGH 15,605 Shared Code Templates with Full Source Exposed (Including Private Ones)
  • HIGH Unauthenticated Access to assets Table - 56,904 Records Exposed
  • MEDIUM Unauthenticated Access to app_updates Table with Signed Download URLs

Private data isn't actually private

3 findings

Personal and sensitive user data is exposed including names and profile data, payment information, IP addresses, private messages. This is the kind of data that triggers mandatory breach notifications under GDPR.

  • CRITICAL 119,709 User Profiles Exposed Including Admin Status and Stripe IDs
  • CRITICAL 1.6M+ Private AI Chat Messages Readable by Anyone
  • CRITICAL Unauthenticated Access to blocked_ips Table Exposes IP Addresses

Secrets baked into your code

2 findings

Figma, Unsplash credentials are hardcoded in the client-side JavaScript bundle. Anyone who views the page source can extract them and make authenticated API calls.

  • CRITICAL Figma Personal Access Token Hardcoded in Client Bundle
  • MEDIUM Unsplash Access Key Hardcoded in Client Bundle

Access controls are broken

3 findings

Security controls like role checks and function permissions have gaps. This can let regular users perform admin actions or access data they shouldn't see.

  • HIGH Sensitive RPC Functions Exposed - get_user_emails Returns User Email Addresses
  • HIGH Private Assets Accessible Without Authentication Due to Missing RLS
  • MEDIUM Anon Key Hardcoded 14 Times in Edge Function Calls

Information leaking out

4 findings

Internal details like staging URLs, API configurations, and analytics data are visible to the public. Attackers use this kind of information to plan more targeted attacks.

  • MEDIUM Staging/Beta Environment URL Leaked in Production Bundle
  • LOW Analytics Configuration Exposed (Umami + Google Analytics)
  • LOW Extensive API Surface Enumeration Possible from Bundle
  • INFO Record Count Leaked via Content-Range Headers

2. Risk Overview

Vulnerability Distribution

Critical
5
High
5
Medium
4
Low
2
Info
1
SeverityCountPercentage
Critical 5 29%
High 5 29%
Medium 4 24%
Low 2 12%
Info 1 6%
Total 17 100%

Risk Categories

CategoryFindingsRisk Level
Mass Data Exposure 1 MEDIUM
Pii Exposure 2 HIGH
Hardcoded Api Key 2 HIGH
Sensitive Data Exposure 1 MEDIUM
Data Exposure 2 HIGH
Missing Rls Policy 2 HIGH
Security Misconfiguration 2 HIGH
Access Control 1 MEDIUM
Information Disclosure 4 CRITICAL

3. Detailed Findings

AI-100 CRITICAL

CATASTROPHIC: 9 Additional Tables Publicly Readable - 2.4M+ Records Exposed

Mass Data Exposure
What this means

Anyone on the internet can read 4.8M+ records from your database without logging in.

Evidence

Probed all 27 discovered tables. 9 additional tables beyond the original 3 have NO RLS and return data to unauthenticated requests: profiles (119,709 records), messages (1,637,674 records), chats (320,869 records), shared_code (15,605 records), components (2,146 records), file_attachments (276,423 records), changelogs (31 records), skills (8 records), code_snippets (8 records). Total: ~2,434,473 records exposed.

Impact

CRITICAL DATA BREACH RISK. The entire application database is effectively public. Most severe exposures: (1) profiles table: 119K user records with full_name, avatar_url, is_admin flag, stripe_customer_id, stripe_subscription_id, subscription_status, is_banned, is_tester flags, daily/monthly usage counters. (2) messages table: 1.6M+ AI chat messages containing full user conversations with the AI including generated HTML/code - this is private user content. (3) chats table: 320K chat sessions with user_id linkage. (4) file_attachments: 276K files with direct download URLs to user-uploaded content. (5) shared_code: 15K templates with full HTML source code, user_id, custom_domain info, DNS verification status. An attacker can trivially: enumerate all users, identify admins (is_admin=true), read all private conversations, download all user files, and map the entire user base with their subscription status.

Recommendation

EMERGENCY: Enable RLS on ALL tables immediately. As an interim measure, consider enabling RLS with a deny-all policy on the most sensitive tables (profiles, messages, chats, file_attachments) and then adding specific allow policies. This is a P0 incident.

AI-101 CRITICAL

119,709 User Profiles Exposed Including Admin Status and Stripe IDs

Pii Exposure
What this means

User names, emails, payment details, profile photos is exposed to anyone who knows where to look.

Evidence

profiles table returns: id, full_name, bio, avatar_url (Google profile photos), pro_subscription, daily_prompt_usage, monthly_standard_prompt_usage, monthly_premium_prompt_usage, subscription_tier, is_admin, stripe_customer_id, stripe_subscription_id, subscription_status, subscription_current_period_end, cancellation_comment, cancellation_feedback, cancellation_reason, slug, views, is_featured, is_tester, is_banned, email (column exists but null in sample), website, location. Sample: 'John D.' with Google avatar URL.

Impact

Complete user directory with PII (names, photos, locations), admin enumeration (is_admin field), payment information (Stripe customer/subscription IDs allow lookup in Stripe), subscription intelligence (who pays, what tier, when they cancel and why), and behavioral data (usage counters). GDPR Article 5 violation - personal data must be processed lawfully with appropriate security.

Recommendation

Enable RLS on profiles immediately. Public profile fields (name, avatar, slug) should have a separate public-facing view. Sensitive fields (is_admin, stripe IDs, usage data, cancellation feedback, is_banned) must NEVER be publicly accessible.

AI-102 CRITICAL

1.6M+ Private AI Chat Messages Readable by Anyone

Pii Exposure
What this means

1.6M+ records containing private messages are publicly accessible.

Evidence

messages table: 1,637,674 records. Columns include: id, content (full message text), is_user, is_system, is_streaming, model (AI model used), timestamp, chat_id, ip. Sample message contains full AI-generated HTML code for a user's project.

Impact

Every conversation every user has ever had with the platform's AI is publicly readable. This includes: proprietary designs and code users asked the AI to generate, potentially confidential business information shared in prompts, user IP addresses (ip column), and the full creative output. This is equivalent to making every user's private workspace public. Massive GDPR, privacy, and intellectual property liability.

Recommendation

RLS policy: messages should only be readable by the chat owner. CREATE POLICY messages_owner_only ON messages FOR SELECT USING (chat_id IN (SELECT id FROM chats WHERE user_id = auth.uid())).

AI-000 CRITICAL

Figma Personal Access Token Hardcoded in Client Bundle

Hardcoded Api Key
What this means

A secret key for Figma is embedded in the public JavaScript. Anyone can extract it and use it.

Evidence

const r0r={figmaApiKey:"figd_****************************"}. Used as fallback in getEffectiveToken(): returns this.getAccessToken()||r0r.figmaApiKey. Calls api.figma.com/v1/files/{id}/nodes with this token.

Impact

CRITICAL - A Figma personal access token (figd_ prefix) is hardcoded in the client bundle as a fallback when no user-provided token exists. Anyone can extract this token and use it to: (1) Read all files in the associated Figma account, (2) Access design assets, (3) Potentially modify files depending on token scope, (4) Access team/org data. This is a direct credential exposure granting authenticated API access to a third-party service.

Recommendation

IMMEDIATELY rotate this Figma API token. Remove it from the client bundle entirely. Implement Figma API access through a server-side edge function that proxies requests with proper user authentication. Never use personal access tokens as fallbacks in client code.

AI-002 CRITICAL

Unauthenticated Access to blocked_ips Table Exposes IP Addresses

Sensitive Data Exposure
What this means

67 records of sensitive data can be read without authorization.

Evidence

67 records accessible without authentication. Sample: ip_address=203.0.113.10, ip_address=203.0.113.25. All 6 columns exposed: id, ip_address, blocked_by (user UUID), reason, created_at, updated_at.

Impact

IP addresses are considered PII under GDPR (EU regulation). The blocked_ips table leaks: (1) IP addresses of blocked users - potentially identifying individuals, (2) UUIDs of admins who performed the blocking via blocked_by field, (3) Timing information of moderation actions. An attacker can enumerate all blocked IPs and correlate admin UUIDs across tables.

Recommendation

Implement RLS policy: CREATE POLICY blocked_ips_admin_only ON blocked_ips FOR SELECT USING (auth.uid() IN (SELECT id FROM profiles WHERE role = 'admin')). This table should never be publicly readable.

AI-103 HIGH

276,423 User-Uploaded Files Accessible via Direct URLs

Data Exposure
What this means

276K+ records are accessible to unauthenticated users.

Evidence

file_attachments table: 276,423 records with file_name, file_url (direct Supabase storage URLs), thumbnail_url, mime_type, file_size, message_id. Sample: hub_app.py at https://xxxxxxxxxxxx.supabase.co/storage/v1/object/public/attachments/...

Impact

All user-uploaded files (images, code, documents) are enumerable and downloadable. The storage URLs use the 'public' bucket path, meaning even if the table gets RLS, the actual files remain downloadable if someone has the URL. Combined with the messages table exposure, attackers can correlate files to specific users and conversations.

Recommendation

Enable RLS on file_attachments. Move the attachments storage bucket from public to private. Use signed URLs with short expiry for file access.

AI-104 HIGH

15,605 Shared Code Templates with Full Source Exposed (Including Private Ones)

Data Exposure
What this means

16K+ records are accessible to unauthenticated users.

Evidence

shared_code table: 15,605 records. Columns: id, slug, title, code (FULL HTML source), language, username, created_at, views, image_url, forks, private, watermark, featured, share_source_code, user_id, category, custom_domain, favicon URLs, DNS verification fields. Records include private=false AND private=true entries.

Impact

All user-created templates/shared code is accessible regardless of privacy setting. This includes: full HTML/CSS/JS source code, custom domain configurations with DNS verification status, user attribution. The 'private' flag is meaningless without RLS - same issue as the assets table. This is likely the 'templates' you were expecting to find.

Recommendation

Enable RLS: public templates WHERE private=false, owner-only WHERE private=true. This is the template/shared code system you mentioned.

AI-003 HIGH

Unauthenticated Access to assets Table - 56,904 Records Exposed

Missing Rls Policy
What this means

A database table with 57K+ records has no row-level security, so anyone can read everything in it.

Evidence

56,904 records accessible without authentication. Filter abuse confirmed (gt operator). All 23 columns exposed including: created_by (user UUIDs), embedding vectors, private flag, premium flag.

Impact

Major data exposure: (1) Private assets (private=true) are readable by anyone - the 'private' flag is meaningless without RLS, (2) created_by field leaks user UUIDs that can be cross-referenced, (3) Full embedding vectors exposed which could be used to reverse-engineer content, (4) Premium content metadata accessible without subscription. Filter abuse allows full data extraction of all 56,904 records.

Recommendation

Implement RLS policies: (1) Public assets: CREATE POLICY assets_public_read ON assets FOR SELECT USING (private = false AND active = true); (2) Private assets: CREATE POLICY assets_owner_read ON assets FOR SELECT USING (auth.uid() = created_by); (3) Consider excluding embedding column from default select.

AI-007 HIGH

Sensitive RPC Functions Exposed - get_user_emails Returns User Email Addresses

Security Misconfiguration
What this means

A security setting is misconfigured, which could let attackers bypass intended restrictions.

Evidence

RPC function get_user_emails called in multiple locations to fetch user email addresses. Used in admin search, user management, and profile lookup. Returns objects with {id, email} fields.

Impact

If this RPC function does not have proper security definer/invoker controls, any authenticated user could call it to enumerate all user email addresses in the system. This would be a serious PII exposure enabling: phishing attacks, spam, account enumeration, and GDPR violations.

Recommendation

Verify that get_user_emails RPC has SECURITY DEFINER with role checks (admin only). Add explicit auth.uid() checks within the function. Consider replacing with server-side-only calls via edge functions.

AI-010 HIGH

Private Assets Accessible Without Authentication Due to Missing RLS

Access Control
What this means

Access controls have a gap that could allow unauthorized actions.

Evidence

Sample data shows assets with private=true are returned in unauthenticated queries. Example: asset ID 54336, title='Red 3D Triangle Symbol Over Snow Landscape', private=true, created_by=d056342e-xxxx-xxxx-xxxx-xxxxxxxxxxxx.

Impact

The 'private' flag on assets is purely cosmetic without RLS enforcement. Any user (or unauthenticated request) can read all assets regardless of the private flag, completely bypassing the intended access control. Users who marked assets as private have a false expectation of confidentiality.

Recommendation

This is urgent. Implement RLS immediately so that private assets are only visible to their creators. The private flag must be enforced at the database level, not just the application layer.

AI-001 MEDIUM

Unsplash Access Key Hardcoded in Client Bundle

Hardcoded Api Key
What this means

A secret key for Unsplash is embedded in the public JavaScript. Anyone can extract it and use it.

Evidence

const qt="****************************" used with Unsplash API via Client-ID header

Impact

Unsplash API key exposed in client-side JavaScript. Can be used to make API requests on behalf of the application, potentially exhausting rate limits (50 req/hr for demo, 5000/hr for production). Attacker could abuse the key for their own image searches.

Recommendation

Move Unsplash API calls to a server-side edge function. Rotate the current key. If client-side access is required, implement a proxy endpoint that rate-limits per user.

AI-004 MEDIUM

Unauthenticated Access to app_updates Table with Signed Download URLs

Missing Rls Policy
What this means

A database table with 16 records has no row-level security, so anyone can read everything in it.

Evidence

16 records accessible. Contains download_url with long-lived signed JWT tokens for Supabase Storage (exp: 2061). Filter abuse confirmed.

Impact

App update records contain signed storage URLs with tokens that expire in ~35 years (exp:2061000485). While the app_updates content itself is semi-public (release notes), the signed URLs are effectively permanent download links. This is likely intentional for a public changelog, but the long-lived tokens are a concern if any update contains sensitive builds.

Recommendation

If this is intentionally public, add explicit RLS: CREATE POLICY app_updates_public_read ON app_updates FOR SELECT USING (true). Use shorter-lived signed URLs (hours, not decades). Verify no pre-release or internal builds are accessible.

AI-005 MEDIUM

Staging/Beta Environment URL Leaked in Production Bundle

Information Disclosure
What this means

Internal information is leaking that could help an attacker plan a more targeted attack.

Evidence

window.location.hostname==="beta--acme-saas-app.netlify.app" with auth callback URLs. Also contains localhost:8080/auth/callback and localhost:9999 references.

Impact

Beta environment hostname disclosed. Attackers can target the staging environment which may have weaker security controls, test data, or debug features enabled. The localhost references suggest development configuration leaked into production.

Recommendation

Remove staging/development URL references from production builds using environment-specific build configurations. Use build-time environment variables to conditionally include these URLs.

AI-008 MEDIUM

Anon Key Hardcoded 14 Times in Edge Function Calls

Security Misconfiguration
What this means

A security setting is misconfigured, which could let attackers bypass intended restrictions.

Evidence

The Supabase anon key appears 14 times directly in fetch() calls to edge functions, rather than using the Supabase client library.

Impact

While the anon key is designed to be public, hardcoding it directly in fetch calls (rather than using createClient) bypasses the Supabase client's built-in auth token refresh and error handling. This pattern suggests some edge function calls may not properly validate the user's JWT on the server side.

Recommendation

Use the Supabase client's functions.invoke() method instead of raw fetch with hardcoded keys. Ensure all edge functions validate the Authorization bearer token server-side.

AI-006 LOW

Analytics Configuration Exposed (Umami + Google Analytics)

Information Disclosure
What this means

Internal information is leaking that could help an attacker plan a more targeted attack.

Evidence

Umami: websiteId=d0e07933-xxxx-xxxx-xxxx-xxxxxxxxxxxx, dashboardUrl=https://cloud.umami.is/share/REDACTED/acme-saas.app. Google Analytics: G-XXXXXXXXXX, GT-XXXXXXXXX, GT-XXXXXXXXX.

Impact

The Umami share URL provides public access to analytics data including page views, visitor counts, referrers, and user behavior patterns. This gives competitors insight into traffic volumes and user engagement. Google Analytics IDs are standard but confirm the analytics stack.

Recommendation

Disable the public Umami share link if analytics data is sensitive. Review what data is visible in the shared dashboard.

AI-009 LOW

Extensive API Surface Enumeration Possible from Bundle

Information Disclosure
What this means

Internal information is leaking that could help an attacker plan a more targeted attack.

Evidence

15 unique edge functions discovered (add-custom-domain, auto-fill-asset-metadata, continue-generation, describe-image, generate-component-metadata, generate-components, generate-edits, generate-html, generate-image, html-to-component, iterate-react-component, react-generator, remove-background-replicate, upscale-image, plus 8 Stripe/newsletter functions). 26 unique RPC functions discovered. 6 storage buckets referenced (assets, components, user-avatars, attachments, preview-images, changelog-images, app-updates).

Impact

Complete API surface map available to attackers from a single JavaScript bundle. This enables targeted attacks against specific endpoints. The newsletter schema reveals a separate Supabase schema, and the pgmq_delete RPC reveals use of a message queue system.

Recommendation

Code-split admin-only functionality into separate bundles loaded only for admin users. Consider server-side rendering for admin features to avoid exposing the full API surface to all users.

AI-011 INFO

Record Count Leaked via Content-Range Headers

Information Disclosure
What this means

Internal information is leaking that could help an attacker plan a more targeted attack.

Evidence

blocked_ips: 67 records, app_updates: 16 records, assets: 56,904 records. Counts exposed via Supabase Content-Range header.

Impact

Exact record counts reveal business metrics: total assets (56K+), blocked users (67), and app versions (16). Competitors can track growth by periodically checking these counts.

Recommendation

For tables with RLS, Supabase automatically hides counts. Implementing RLS (as recommended above) will resolve this. For intentionally public tables, consider if count exposure is acceptable.

4. Compliance Mapping

GDPR

NON-COMPLIANT
Art. 5 (Data Processing Principles), Art. 32 (Security of Processing), Art. 25 (Data Protection by Design), Art. 33 (Breach Notification)

SOC 2

NON-COMPLIANT
CC6.1 (Logical Access), CC7.2 (System Monitoring), CC6.3 (Role-Based Access)

PCI-DSS

NON-COMPLIANT
Req. 6 (Secure Systems), Req. 7 (Restrict Access), Req. 3 (Protect Stored Data)

5. Remediation Roadmap

Immediate (24-48 hours) — 5 items

  • CATASTROPHIC: 9 Additional Tables Publicly Readable - 2.4M+ Records Exposed: EMERGENCY: Enable RLS on ALL tables immediately. As an interim measure, consider enabling RLS with a deny-all policy on the most sensitive tables (profiles, messages, chats, file_attachments) and then adding specific allow policies. This is a P0 incident.
  • 119,709 User Profiles Exposed Including Admin Status and Stripe IDs: Enable RLS on profiles immediately. Public profile fields (name, avatar, slug) should have a separate public-facing view. Sensitive fields (is_admin, stripe IDs, usage data, cancellation feedback, is_banned) must NEVER be publicly accessible.
  • 1.6M+ Private AI Chat Messages Readable by Anyone: RLS policy: messages should only be readable by the chat owner. CREATE POLICY messages_owner_only ON messages FOR SELECT USING (chat_id IN (SELECT id FROM chats WHERE user_id = auth.uid())).
  • Figma Personal Access Token Hardcoded in Client Bundle: IMMEDIATELY rotate this Figma API token. Remove it from the client bundle entirely. Implement Figma API access through a server-side edge function that proxies requests with proper user authentication. Never use personal access tokens as fallbacks in client code.
  • Unauthenticated Access to blocked_ips Table Exposes IP Addresses: Implement RLS policy: CREATE POLICY blocked_ips_admin_only ON blocked_ips FOR SELECT USING (auth.uid() IN (SELECT id FROM profiles WHERE role = 'admin')). This table should never be publicly readable.

Short-term (1-2 weeks) — 5 items

  • 276,423 User-Uploaded Files Accessible via Direct URLs: Enable RLS on file_attachments. Move the attachments storage bucket from public to private. Use signed URLs with short expiry for file access.
  • 15,605 Shared Code Templates with Full Source Exposed (Including Private Ones): Enable RLS: public templates WHERE private=false, owner-only WHERE private=true. This is the template/shared code system you mentioned.
  • Unauthenticated Access to assets Table - 56,904 Records Exposed: Implement RLS policies: (1) Public assets: CREATE POLICY assets_public_read ON assets FOR SELECT USING (private = false AND active = true); (2) Private assets: CREATE POLICY assets_owner_read ON assets FOR SELECT USING (auth.uid() = created_by); (3) Consider excluding embedding column from default select.
  • Sensitive RPC Functions Exposed - get_user_emails Returns User Email Addresses: Verify that get_user_emails RPC has SECURITY DEFINER with role checks (admin only). Add explicit auth.uid() checks within the function. Consider replacing with server-side-only calls via edge functions.
  • Private Assets Accessible Without Authentication Due to Missing RLS: This is urgent. Implement RLS immediately so that private assets are only visible to their creators. The private flag must be enforced at the database level, not just the application layer.

Medium-term (30 days) — 4 items

  • Unsplash Access Key Hardcoded in Client Bundle: Move Unsplash API calls to a server-side edge function. Rotate the current key. If client-side access is required, implement a proxy endpoint that rate-limits per user.
  • Unauthenticated Access to app_updates Table with Signed Download URLs: If this is intentionally public, add explicit RLS: CREATE POLICY app_updates_public_read ON app_updates FOR SELECT USING (true). Use shorter-lived signed URLs (hours, not decades). Verify no pre-release or internal builds are accessible.
  • Staging/Beta Environment URL Leaked in Production Bundle: Remove staging/development URL references from production builds using environment-specific build configurations. Use build-time environment variables to conditionally include these URLs.
  • Anon Key Hardcoded 14 Times in Edge Function Calls: Use the Supabase client's functions.invoke() method instead of raw fetch with hardcoded keys. Ensure all edge functions validate the Authorization bearer token server-side.

Long-term (Ongoing) — 3 items

  • Analytics Configuration Exposed (Umami + Google Analytics): Disable the public Umami share link if analytics data is sensitive. Review what data is visible in the shared dashboard.
  • Extensive API Surface Enumeration Possible from Bundle: Code-split admin-only functionality into separate bundles loaded only for admin users. Consider server-side rendering for admin features to avoid exposing the full API surface to all users.
  • Record Count Leaked via Content-Range Headers: For tables with RLS, Supabase automatically hides counts. Implementing RLS (as recommended above) will resolve this. For intentionally public tables, consider if count exposure is acceptable.
  • Implement continuous security monitoring
  • Schedule regular security assessments

6. Appendix

A. Testing Methodology

This audit was performed using a combination of automated scanning and AI-driven analysis:

  1. Reconnaissance and asset discovery via public endpoints
  2. Client-side JavaScript bundle analysis for exposed secrets and API surfaces
  3. Database access control testing (RLS policy validation)
  4. Unauthenticated data access probing across all discovered tables
  5. Sensitive data classification and PII exposure assessment
  6. Compliance mapping against GDPR, SOC 2, and PCI-DSS frameworks

B. Glossary

RLSRow Level Security — PostgreSQL feature to restrict row access based on policies
PIIPersonally Identifiable Information — data that can identify an individual
Anon KeySupabase anonymous API key for unauthenticated public access
CVSSCommon Vulnerability Scoring System
CWECommon Weakness Enumeration
GDPRGeneral Data Protection Regulation (EU)
SOC 2Service Organization Control Type 2
PCI-DSSPayment Card Industry Data Security Standard
RPCRemote Procedure Call — server-side functions callable via API

C. References