Skip to main content

ShipSafe

ShipSafe
ShipSafe
LovableSupabaseSecurity

5 Security Vulnerabilities Every Lovable App Has (And How to Fix Them)

Lovable builds beautiful Supabase apps fast. But it consistently misses Row Level Security, leaks service role keys, and more. Here's the fix for each.

7 min read

Lovable is one of the fastest ways to build a full-stack web app. Describe what you want, and it generates a React frontend backed by Supabase with authentication, database tables, and deployable infrastructure. For prototyping, it is extraordinary.

For production? It is a security incident waiting to happen.

We scanned dozens of Lovable-generated apps with ShipSafe and found the same five vulnerabilities appearing in nearly every project. These are not edge cases. They are structural patterns in how Lovable scaffolds Supabase applications, and they leave your users' data exposed.

This guide covers each vulnerability with exact code examples and the specific fix. If you have shipped a Lovable app, read this before your next deploy.

1. Missing Row Level Security (RLS) on Supabase Tables

CriticalFound in 89% of Lovable apps

This is the number one Lovable security flaw. When Lovable creates Supabase tables, it runs CREATE TABLEstatements but does not enable Row Level Security or create any RLS policies. Without RLS, Supabase's PostgREST layer allows any authenticated user to perform any operation on any row in the table.

This means if User A is logged into your app, they can read, modify, or delete User B's data by simply calling the Supabase API directly — bypassing your frontend entirely.

What Lovable Generates

-- Lovable-generated migration
CREATE TABLE public.projects (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id),
  name TEXT NOT NULL,
  data JSONB,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- That's it. No RLS. No policies.
-- Any authenticated user can: SELECT, INSERT, UPDATE, DELETE
-- on ALL rows, not just their own.

The Fix

-- Step 1: Enable RLS
ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;

-- Step 2: Create policies
CREATE POLICY "Users can view own projects"
  ON public.projects FOR SELECT
  USING (auth.uid() = user_id);

CREATE POLICY "Users can create own projects"
  ON public.projects FOR INSERT
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update own projects"
  ON public.projects FOR UPDATE
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can delete own projects"
  ON public.projects FOR DELETE
  USING (auth.uid() = user_id);

You need RLS policies on every tablethat stores user data. This includes junction tables, settings tables, and audit logs. The Supabase dashboard makes it easy to verify: go to Table Editor, click a table, and check the “RLS Enabled” badge. If it says “RLS Disabled,” your data is exposed. This vulnerability maps to OWASP A01:2021 Broken Access Control.

2. Service Role Key Exposed in Client-Side Code

CriticalFound in 34% of Lovable apps

Supabase has two keys: the anon key (safe for client-side, respects RLS) and the service_role key (bypasses all RLS, full database access). Lovable sometimes uses the service role key in client-side Supabase initialization, especially when the generated code needs to perform admin operations.

The service role key in client code is visible to anyone who opens browser DevTools. With this key, an attacker has unrestricted access to your entire database, bypassing every security policy you set up.

Vulnerable Code

// src/integrations/supabase/client.ts (Lovable-generated)
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY // DANGER!
  // This key bypasses ALL RLS policies
);

The Fix

// Client-side: use the anon key ONLY
export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY // Safe: respects RLS
);

// Server-side only (Edge Function or API route):
import { createClient } from "@supabase/supabase-js";
const supabaseAdmin = createClient(
  Deno.env.get("SUPABASE_URL")!,
  Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! // OK: server-only
);

Check your .env file right now. If any variable prefixed with VITE_ contains a service role key, it is exposed to the browser. Remove it and create a Supabase Edge Function for any operation that requires elevated privileges.

3. Direct Queries to auth.users Table

HighFound in 41% of Lovable apps

Lovable frequently generates code that queries the auth.users table directly from the client to display user profiles, show team member lists, or populate dropdown menus. The auth.users table contains email addresses, phone numbers, metadata, and authentication provider details for every user in your system.

Vulnerable Code

// Lovable-generated: fetching user profiles
const { data: users } = await supabase
  .from("users")  // Points to auth.users via a view
  .select("id, email, raw_user_meta_data")
  .order("created_at");

// Exposes ALL user emails and metadata to any authenticated user

The Fix

-- Create a public profiles table with only safe fields
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users(id) PRIMARY KEY,
  display_name TEXT,
  avatar_url TEXT
);

-- Enable RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- Allow users to read all profiles (public info only)
CREATE POLICY "Profiles are viewable by everyone"
  ON public.profiles FOR SELECT
  USING (true);

-- Users can only update their own profile
CREATE POLICY "Users can update own profile"
  ON public.profiles FOR UPDATE
  USING (auth.uid() = id);

-- Auto-create profile on user signup (trigger)
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, display_name)
  VALUES (NEW.id, NEW.raw_user_meta_data->>'full_name');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Never query auth.users from client code. Create a separate profiles table that contains only the information you want to be publicly visible, protected by its own RLS policies.

4. Secrets in VITE_ Environment Variables

HighFound in 27% of Lovable apps

Lovable generates Vite-based React apps. In Vite, any environment variable prefixed with VITE_is bundled into the client-side JavaScript and visible to anyone who inspects your app's source. Lovable sometimes places API keys for third-party services — Stripe secret keys, OpenAI keys, SendGrid keys — behind VITE_ prefixes.

Vulnerable .env file

# .env (Lovable-generated)
VITE_SUPABASE_URL=https://abc.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOi...       # OK: public key
VITE_STRIPE_SECRET_KEY=sk_live_51N8...      # EXPOSED!
VITE_OPENAI_API_KEY=sk-proj-abc123...       # EXPOSED!
VITE_SENDGRID_API_KEY=SG.abc123...          # EXPOSED!

The Fix

# .env - only public values get VITE_ prefix
VITE_SUPABASE_URL=https://abc.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOi...

# Secret keys: NO VITE_ prefix (server-only)
STRIPE_SECRET_KEY=sk_live_51N8...
OPENAI_API_KEY=sk-proj-abc123...
SENDGRID_API_KEY=SG.abc123...

# Access via Supabase Edge Functions:
# Deno.env.get("STRIPE_SECRET_KEY")

Audit your .env file now. The rule is simple: if you would not print it on a billboard, do not prefix it with VITE_. Only truly public values like the Supabase URL and anon key should be client-side. Everything else belongs in Supabase Edge Functions or a backend server.

5. No Input Validation or Sanitization

MediumFound in 73% of Lovable apps

Lovable generates forms with basic HTML structure but almost never includes input validation or sanitization. User input flows directly from form fields into database queries and API calls without type checking, length limits, or content filtering.

This opens the door to SQL injection via Supabase's filter operators, stored XSS if user content is rendered without escaping, and denial-of-service via oversized payloads.

Vulnerable Code

// Lovable-generated form handler
async function handleSubmit(e) {
  e.preventDefault();
  const formData = new FormData(e.target);

  // Direct insert with NO validation
  await supabase.from("feedback").insert({
    name: formData.get("name"),       // No length limit
    email: formData.get("email"),     // No format check
    message: formData.get("message"), // No sanitization
    rating: formData.get("rating"),   // No type check
  });
}

The Fix (using Zod)

import { z } from "zod";

const feedbackSchema = z.object({
  name: z.string().min(1).max(100).trim(),
  email: z.string().email().max(255),
  message: z.string().min(1).max(5000).trim(),
  rating: z.coerce.number().int().min(1).max(5),
});

async function handleSubmit(e) {
  e.preventDefault();
  const formData = new FormData(e.target);
  const raw = Object.fromEntries(formData);

  // Validate and sanitize
  const result = feedbackSchema.safeParse(raw);
  if (!result.success) {
    setErrors(result.error.flatten().fieldErrors);
    return;
  }

  await supabase.from("feedback").insert(result.data);
}

Use Zod or a similar validation library on every form submission and API call. Validate on the client for user experience, and validate again on the server (or in a Supabase Edge Function) for security. This protects against CWE-20: Improper Input Validation.

Vulnerability Summary

VulnerabilitySeverityPrevalence
Missing RLS PoliciesCritical89%
Service Role Key in ClientCritical34%
Direct auth.users QueriesHigh41%
Secrets in VITE_ Env VarsHigh27%
No Input ValidationMedium73%

How to Audit Your Lovable App in 5 Minutes

You do not need to be a security expert to fix these issues. Here is a quick audit checklist:

  1. Open the Supabase dashboard. Go to Table Editor. Check every table for the “RLS Enabled” badge. Enable RLS and add policies for any table missing them.
  2. Search your codebase for service_role. If it appears anywhere outside of a Supabase Edge Function, move it.
  3. Search for queries to auth.users or a users view that proxies it. Replace with a dedicated profiles table.
  4. Open your .env file. Every VITE_ variable should be something you would show publicly. Move everything else server-side.
  5. Add Zod validation to every form handler and API call. Start with your most sensitive forms (signup, payment, settings).

Or, for an automated approach, run ShipSafe on your repo. It catches all five of these issues and generates specific fix recommendations for your codebase. For more AI code security tips, see our vibe coding security checklist.

Bottom Line

Lovable is a powerful prototyping tool. It lets non-technical founders go from idea to deployed app in hours. But the security gaps it leaves are real and exploitable, and they put your users' data at risk.

The good news: every one of these vulnerabilities has a straightforward fix. Enable RLS, move secrets server-side, create a profiles table, audit your env vars, and add input validation. Do these five things and your Lovable app goes from a security liability to a properly hardened production application.

Want to check your own app?

Paste your GitHub URL and get a security report in under 2 minutes. Free scan, no credit card required.

Scan My App Free