BLOG

Security Anti-Patterns Caused by AI Coding Tools

Agentic platforms like Lovable, Claude, and Codex cause unexpected security anti-patterns

By c0a15726-c5b1-4b0d-85e6-fe15553df9e2 ·

Security Anti-Patterns Caused by AI Coding Tools

Last week was tough for vibe coders and people who use tools like Lovable and Vercel to build and deploy applications.

I want to talk about why last week was so shitty for so many of us, and I’m gonna go into detail. Prepare yourselves.

Lovable and Vercel incidents

On April 20, Vercel was compromised. It turns out a Vercel employee used the AI platform Context.ai, and an Context employee was infected with the Lumma stealer malware. The Vercel employee gave Context "Allow All" access to their Google Workspace as part of the Context onboarding workflow. From there the threat actors got access to Vercel’s infrastructure in a yet unexplained pivot.

Thousands of Vercel customers had to quickly learn how to do cloud-native incident response:

  • Identifying and rotating any credentials you had saved in Vercel. It turns out that build processes need a lot of credentials!

  • Analyzing Vercel activity and deployment logs

  • Rotating any Vercel personal access tokens (PATs)

  • Adding MFA and hardening your Vercel environments.

Then on April 21, Lovable announced that it had a security incident. They shipped a backend change in February 2026 that reintroduced public access to chat histories and source code on public projects. A fix shipped within two hours of the public report, and on April 23 they published a formal incident report acknowledging the chain. Remediation steps included reverting the regression, making all historically public projects private by default (except official templates), and emailing owners of projects that had been viewed by other accounts during the window. They are still working through the access-log review to determine which public projects were actually accessed by non-owners. Third-party guidance was to assume any credentials embedded in a pre-April-20 public project source (especially Supabase service keys) were exposed, and to rotate them. It also recommended grepping AI chat history for any secrets pasted mid-build.

  • Root cause. A Broken Object Level Authorization (BOLA) bug: /projects/{id}/* endpoints validated Firebase auth but did not verify project ownership, so any logged-in user could access other tenants’ project data by ID. Lovable had fixed this problem before, but unfortunately, a backend permissions-unification change regressed an earlier fix and re-enabled access to chats on public projects.

  • Blast radius. Source code and AI chat history (including embedded secrets) were exposed for public projects during the window; private projects and Lovable Cloud were not. Unfortunately, this meant that anything customers shared in their chat conversations with Lovable was potentially exposed. Customers routinely add API keys, passwords and other sensitive data to the chat as Lovable will ask for these things

  • Disclosure failure. Lovable has a bug bounty program with HackerOne and researchers started reporting the regression on Feb 22, 2026, but reports were closed because triage docs incorrectly said public chat visibility was intended behaviour. Lovable did not recognize the regression until it surfaced on X, and they re-fixed it April 20th.

  • Response. Initial PR was rough: Lovable denied a breach, called the exposure intentional, blamed documentation, and then blamed HackerOne.

Screenshot 2026-04-26 at 9.24.29 pm.png

OpenSourceMalware (OSM) uses some of these platforms, and as part of our own research and incident response we wanted to look at how some of our own infrastructure and applications were configured. Was there additional hardening that we could do? As part of that incident response the OSM team wrote and published a Vercel security incident response playbook, which you can find on GitHub.

AI full-stack development platforms select for insecure defaults

The rest of this blog covers what we learned from this experience...and it's not great. When an agentic platform “builds you a SaaS,” it isn’t just generating code — it is making architecture and integration decisions the way a human staff engineer would. The problem is that these platforms are optimized for time-to-green (a working demo, a successful deploy, fewer steps), and the secure version of “make it work” almost always requires more steps: more configuration, more secrets hygiene, more authorization checks, more review.

So the issue isn’t only that anti-patterns appear — it’s that the product loops and scaffolding patterns systematically select for insecure integration shortcuts, and then reproduce them at scale.

The anti-pattern taxonomy

  • Credential anti-patterns: secret sprawl, public bundling, hardcoded fallbacks

  • Authorization anti-patterns: BOLA, missing ownership checks, permissive or missing RLS

  • Exposure anti-patterns: public buckets, overly-broad project visibility, chat/source leakage

  • Supply-chain anti-patterns: stale scaffolds, hallucinated/typosquatted dependencies

  • Webhook anti-patterns: handlers without signature verificationAgentic platforms have known patterns

While using agentic coding platforms our team has noticed a theme across many of them: Regardless of whether you are using Lovable, Claude, Codex, Curser, or something else, part of the value these platforms provide is their experience building SaaS apps. You are paying them, in part, to suggest patterns, and make opinionated choices about infrastructure.

And here’s where it gets interesting: When you ask any of these tools to "build me a SaaS," the answer collapses to roughly the same dependency graph: Next.js or React+Vite, Tailwind, shadcn/ui, a Postgres BaaS (Supabase or Neon), Clerk or Auth.js, Stripe, Resend or SendGrid for email, Vercel or Netlify for hosting.

Agentic Coding Platform Default Stacks

This blueprint is the canonical 2025–2026 vibe-coded SaaS stack, but each platform has its unique way of laying down that infrastructure. To understand these patterns and why they come with security problems, we needed to document and describe these patterns for you.

Here’s our breakdown and analysis of the different stack patterns that vibe-coding platforms are using. This is not to say that if you ask Lovable for a specific build architecture, that it won’t deviate from its standard pattern. But, if you don’t specify, based on our experience, this is what you’ll get:

Platform

Frontend default

UI library

Backend / Data

Auth

Deployment target

Lock-in shape

Lovable

React + Vite + TypeScript (not Next.js)

Tailwind + shadcn/ui + Radix

Supabase (Postgres + Edge Functions in Deno)

Supabase Auth, sometimes Clerk

Lovable hosted preview, GitHub export

Heavy Supabase dependency; anon key shipped to client

v0 (Vercel)

Next.js App Router + TypeScript

Tailwind + shadcn/ui

Next.js Server Actions / API routes; Drizzle + Neon, or Supabase

Clerk, Auth.js (NextAuth), Stack Auth

Vercel

Tight Vercel ecosystem coupling

Bolt.new

React + Vite by default; offers Next/Vue/Svelte/Astro/Remix

Tailwind

WebContainer-based: Hono/Express in browser, or Supabase

Variable, often Supabase Auth

Netlify (default), Cloudflare Pages

WebContainer constraints: no native binaries, limited Python, no real git (uses isomorphic-git)

Base44

React (not user-selectable)

Proprietary component set

Their own managed BaaS: DB, auth, email, SMS all integrated

Built-in

Their hosted infra only

Total platform lock-in; code not exportable on free tier

Replit Agent

Multi-language; for web typically React + Node/Express, or Flask/FastAPI for Python

Tailwind common

Replit DB or Neon Postgres

Replit Auth (proprietary)

Replit Autoscale / Reserved VM

Replit Secrets manager, hosted DB

Cursor (IDE)

Whatever’s in the repo; greenfield → Next.js + TS

Tailwind + shadcn/ui

Prisma + Postgres (Neon, Supabase, PlanetScale)

Clerk or Auth.js

Vercel

Mostly conventions, not platform lock-in

Windsurf (IDE)

Same as Cursor

Tailwind + shadcn/ui

Same

Same

Vercel/Railway

Same

Codex / GPT-5 codex CLI

Similar to Cursor; OpenAI training leans Python harder: FastAPI is reflexive

Tailwind

Postgres + SQLAlchemy or Drizzle

Auth.js, Clerk

Vercel, Render

Conventions, follows repo

Claude Code / Claude chat

JS: Next.js + TS + Tailwind + Prisma. Python: FastAPI + Pydantic + SQLAlchemy

Tailwind + shadcn/ui

Postgres (Supabase/Neon) for JS; Postgres + SQLAlchemy for Python

Clerk, Auth.js, lucia-auth

JS → Vercel, Python → Railway/Render/Fly.io

Conventions, follows repo

Client-direct vs. server-mediated backends

The biggest structural security difference between these platforms isn’t framework choice: it’s whether the backend is client-direct or server-mediated. Lovable, Bolt, and any “React + Vite + Supabase” output put the database one HTTP call away from the user’s browser, with security enforced entirely by Postgres RLS policies the AI may or may not have written correctly. CVE-2025-48757 (Lovable’s RLS misconfiguration class), the Vercel breach’s environment-variable angle, and most BOLA findings in this category all reduce to the same pattern. Server-mediated stacks (Next.js Server Actions, FastAPI, Express) at least force authorization through middleware the model has to actively skip rather than forget.

Secrets handling

Another thing to consider is how the AI development platforms integrate with the platforms they are using to store and access secrets. We have found that each of the platforms has its own pattern for what it suggests, and then what it falls back to if there is any resistance in deploying an application.

  • v0 / Vercel: secrets in Vercel project env vars; the April 2026 incident showed these can be enumerated if not marked “sensitive”

  • Lovable: historically embedded Supabase keys in source code that turned out to be readable by other tenants

  • Replit: secrets manager built in

  • IDE tools (Cursor, Claude Code, Windsurf, Codex): rely on the user’s local .env discipline

In fact, many of these tools will fall back to creating .env files in the local source repo, which then get pushed to GitHub, GitLab, etc. Even worse, sometimes those .env files get added to the web content and can be found via scanning for file paths in URLs.

Dependency profile

Vite-based stacks pull in vite, @vitejs/plugin-react, postcss, tailwindcss. Next.js stacks pull in the larger Next runtime, swc, and a deeper transitive tree. shadcn/ui isn’t itself a dependency (it’s copy-paste components) but pulls Radix UI primitives, lucide-react, class-variance-authority, and tailwind-merge: five-or-so packages that show up in essentially every vibe-coded app and would make extremely high-leverage targets for a TeamPCP-style postcss-adjacent compromise.

Python axis

Narrower than the JS side. Claude defaults to FastAPI + uvicorn + Pydantic + SQLAlchemy + Alembic, deployed to Railway or Render, sometimes Fly.io. Codex pushes a similar stack. Cursor follows whatever’s already in the repo. Replit defaults to Flask for simple work and FastAPI when async or typing comes up. None of the no-code builders (Lovable / Bolt / Base44 / v0) generate Python services as a primary deliverable: Lovable explicitly tells users to export and rebuild the backend if they need Python, and Base44 abstracts the backend entirely.

Integration is where the risk concentrates

The platforms above don't just pick a stack: they wire it together, which means the agent is making integration decisions a human architect would normally make. This is the layer where the most consistently insecure choices get baked in, because the model is solving for "make it work" and the secure version of "make it work" usually requires more steps. A few patterns recur across every agentic platform regardless of which underlying services get bundled.

Service-role keys end up in source code

The canonical example: Lovable + Supabase. Supabase issues two keys per project: the anon key, designed to be shipped to the client and restricted by Row Level Security, and the service_role key, which bypasses RLS entirely and is meant only for trusted server contexts. These keys are named poorly, and both humans and AI agents conflate them constantly. When the model needs to perform an "admin" operation (create a user record from a webhook, run a migration, seed data), it reaches for service_role and frequently instantiates the Supabase client with it in code paths that get bundled into the browser, or commits the key to a .env.example, or pastes it into the chat history of a public project: which is exactly what made the recent Lovable disclosure so catastrophic. This pattern is not Lovable-specific; it shows up any time an agent wires a service that has both a public and a privileged key (Stripe pk_ vs sk_, OpenAI / Anthropic keys, SendGrid full-access vs restricted, Twilio account SID + auth token).

What's causing the anti-patterns

This “how the anti-pattern is born” loop is painfully consistent:

  • You ask for a real feature (“admin dashboard”, “image upload”, “Stripe webhook”).

  • The agent hits friction (auth errors, CORS, RLS denies, missing env vars).

  • The agent applies the shortest bypass that restores forward progress (service-role key, public bucket, permissive RLS, NEXT_PUBLIC_* secret).

  • The app works — and the bypass becomes the production security posture.

Even when you have defined a Supabase Edge Function Secret, which is the suggested best practices for these types of key, they often still end up exposed in source code for two reasons:

  • The AI agents will bypass the best practices when they hit any kind of obstacle or resistance. it is their job to find a way to deliver success for your prompt, and if that means they have to add the service_role key to source code to do it, they will.

  • The developer, doesn’t understand that a “service_role” key is supposed to be secret. You can see that this is a thing by searching GitHub for these keys. They are everywhere, and yes, agents are adding them directly to source code, but so are developers.

github-dev-committed-keys.png

Public env var prefixes used as a workaround

Both Vite (VITE_*) and Next.js (NEXT_PUBLIC_*) inline any env var with the matching prefix into the client bundle at build time. The intended use is publishable keys and feature flags. The actual use, when an agent needs something "available in the browser," is to prefix the variable and move on. The result is NEXT_PUBLIC_OPENAI_API_KEY and similar in production bundles, retrievable by anyone who opens DevTools. This is the modern equivalent of git grep -r AWS_SECRET on GitHub.

RLS off or permissive by default

Row-level Security, or RLS, is a permissioning system that restricts access to database table rows based on policies that the agent (or human) creates.

The reality is that administering RLS in a secure way is difficult. This is particularly true if you have never had to manage a database before. Especially a database that allows direct access via HTTP. When Lovable or Bolt creates Supabase tables on a user's behalf, RLS is frequently either not enabled or enabled with policies like USING (true) or USING (auth.uid() IS NOT NULL): the latter restricts to authenticated users but does nothing about cross-tenant access. The agent has fulfilled the literal request ("let logged-in users read their data") without addressing the implicit one ("only their own data"). The CVE-2025-48757 cluster and the bulk of the BOLA findings in vibe-coded apps sit on top of this exact failure mode.

Even worse, Supabase says that they have enabled RLS by default for new tables in 2026, but that’s not been the experience of many users, including this team. In fact, just today I got this pop up in Supabase.

Wait, I thought it was enabled by default? Is it not? Who knows!

Screenshot 2026-04-27 at 6.40.27 am.png

Bad package choices

Agentic platforms routinely pick dependencies by pattern-matching training data, not by verifying what is current today. Two failure modes show up constantly:

  • Out-of-date “best practice” stacks: the agent installs older major versions or deprecated packages because they were common when the model last saw lots of examples. The result is breakage (wrong APIs), missed security fixes, and copy-pasted configs that no longer match the ecosystem.

  • Hallucinated package names: the agent confidently tells you to npm i or pip install libraries that do not exist, were renamed, or are unofficial forks. Teams then waste time chasing 404s, or worse, they grab a similarly named third-party package and accidentally pull in something unvetted (a classic typosquat opportunity). Even worse, bad guys have picked up on this and routinely create “slop-squatted” packages that match these hallucinated package names.

Practical takeaway: treat every agent-suggested dependency list as untrusted input. Verify that each package actually exists, is maintained, and is the intended official project. Prefer pinning versions, and run npm audit / pip-audit early—before the stack calcifies into production. Oh, and yes, reference OpenSourceMalware to verify that the packages you are using are not malicious.

Storage buckets flipped to public

Supabase Storage and S3 buckets default to private, but agents flip them to public when wiring up image uploads because the alternative: signed URLs, server-side proxying, or RLS policies on the storage schema: is more code. A public Supabase bucket on a vibe-coded app is essentially an open S3 in 2013 terms, and the bucket URL pattern is predictable enough to enumerate.

Screenshot 2026-04-27 at 7.07.09 am.png

Webhook handlers without signature verification

Every payment, email, and source-control integration in the canonical SaaS template ships a webhook endpoint. The signature verification step (stripe.webhooks.constructEvent, GitHub's X-Hub-Signature-256, Resend's signing secret) is consistently skipped by agentic generators because it requires a separate env var, a raw-body parser configuration, and an extra try/catch. The shipped endpoint accepts and trusts any JSON-shaped payload at the documented URL. For Stripe specifically, this turns a webhook handler that grants premium access into a public "give me premium access" endpoint.

Hardcoded fallback secrets

process.env.JWT_SECRET || "secret" and os.environ.get("SECRET_KEY", "dev-key-change-in-prod") are reflexive patterns across nearly every agent. They exist because the model is trying to make the app run on first boot without configuration, but they survive into production deployments where the env var was never set and the fallback becomes the actual signing secret. A grep across deployed agent-generated apps for literal strings like "your-secret-here" or "changeme" is a viable threat-hunting query.

What this means for your vibe-coded app

The integration-layer failures above are not detectable as a malicious package or a CVE: they're configuration vulnerabilities baked in by the generator. But they create a predictable, fingerprintable exposure pattern per platform: a Lovable app probably has a service_role key somewhere it shouldn't, a v0 app probably has a NEXT_PUBLIC_* secret leak risk, a Bolt + Supabase app probably has RLS gaps, anything with a Stripe webhook probably skips signature verification. From a threat intelligence standpoint, this is closer to "default credentials in embedded devices" than to traditional package compromise: the surface area is created by the generator, not by an attacker, and it's consistent enough across instances to be enumerated.