← All posts
·8 min read

Is Your Bolt.new App Secure? A Complete Security Guide

Bolt.new ships working apps in minutes. But wildcard CORS, exposed API keys, and missing validation are baked into the defaults. Here's how to fix every major Bolt security issue.

Bolt.new is genuinely impressive. You describe what you want, and in minutes you have a working full-stack app with a real database, real auth, and a real deployment. For founders who aren't developers, it's a superpower.

But Bolt ships fast by making assumptions. Some of those assumptions — wildcard CORS, API keys in frontend code, no rate limiting — create security vulnerabilities that attackers actively look for in Bolt-built apps.

This guide covers every significant security issue in Bolt.new apps and exactly how to fix each one.


The core Bolt security problem

Bolt generates code that works. Working code and secure code are different things.

When Bolt creates a backend API route, it focuses on making the feature functional. It doesn't add authentication middleware by default, because authentication would require it to know who should be allowed to call the route — and it doesn't know that. When Bolt integrates a third-party API, it calls that API directly from the frontend because that's the simplest path. When Bolt sets up CORS, it uses wildcard because that removes friction during development.

None of this is careless — it's the correct tradeoff for getting something working. But before your app has real users and real data, these defaults need to be reviewed and fixed.


Issue 1: Wildcard CORS (the most common Bolt vulnerability)

What it is: Your API responds to requests from any website on the internet.

When you see Access-Control-Allow-Origin: * in your API responses, it means any website — not just yours — can make requests to your API using your users' credentials.

Here's why that matters: if a logged-in user of your app visits a malicious website, that website can silently call your API using the user's credentials. Read their data, change their settings, make purchases on their behalf.

How to check: Open Chrome DevTools on your app, go to Network, click any API request, and look at the Response Headers for Access-Control-Allow-Origin.

How to fix it: In your Bolt-generated API routes, replace wildcard CORS with your specific domain:

// In your API route or middleware
const allowedOrigin = 'https://your-app.bolt.new' // or your custom domain

headers.set('Access-Control-Allow-Origin', allowedOrigin)
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')

If you're using Supabase, go to your Supabase project → Settings → API → Add Site URL, and add only your specific app URL. Remove any wildcard entries.


Issue 2: API keys in frontend JavaScript

What it is: Secret credentials that are visible to anyone who opens your browser's DevTools.

Bolt often generates code that calls third-party APIs directly from the browser. When it does, it has to put the API key somewhere in the JavaScript. That JavaScript runs in users' browsers. Anyone can view it.

Common examples:

  • OpenAI API key (sk-...) for AI features
  • Stripe secret key for payment processing
  • SendGrid API key for email
  • Any API key that gives access to a paid or sensitive service

How to check: Open your app, right-click → View Page Source, and search for common key patterns: sk-, pk_live_, SG., key_.

How to fix it: Move all API calls to your server. In Bolt, create a new API route that handles the third-party API call server-side, and have your frontend call your own route instead:

// ❌ Wrong: OpenAI key in frontend code
const response = await fetch('https://api.openai.com/v1/chat/completions', {
  headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` }
})

// ✅ Right: Call your own API route
const response = await fetch('/api/ai/chat', {
  method: 'POST',
  body: JSON.stringify({ message })
})

Your own /api/ai/chat route runs on the server and can safely use environment variables that never reach the browser.


Issue 3: Missing security headers

What they are: HTTP response headers that tell the browser how to behave securely.

Bolt's default deployment doesn't include security headers. Without them:

  • No X-Frame-Options: Your app can be embedded in a hidden iframe and used to steal clicks (clickjacking)
  • No Content-Security-Policy: If an attacker finds an XSS bug, they can run any script on your page
  • No Strict-Transport-Security: Browsers won't automatically use HTTPS for future visits
  • No X-Content-Type-Options: Browsers might execute files as a different type than intended

How to fix it: If your Bolt app is deployed on Netlify or Vercel, add a configuration file:

For Vercel, create vercel.json in your project root:

{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "X-Frame-Options", "value": "DENY" },
        { "key": "X-Content-Type-Options", "value": "nosniff" },
        { "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
        { "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" }
      ]
    }
  ]
}

For Netlify, create netlify.toml:

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"
    Strict-Transport-Security = "max-age=31536000; includeSubDomains"

Issue 4: No authentication on API routes

What it is: Backend routes that return or modify user data without checking who's asking.

Bolt generates API routes that work — but "working" often means the happy path, where the right user calls the right route. It doesn't always mean the route verifies that the caller is authenticated before proceeding.

An unauthenticated route that returns user data will return that data to anyone who calls it, not just logged-in users.

How to check: Open your browser's DevTools → Network. Log out of your app, then try calling your API routes directly with Fetch or copy-paste the request URL. If you get data back without being logged in, the route isn't protected.

How to fix it: Every route that returns or modifies user data needs to check authentication:

export async function GET(request: Request) {
  // Check authentication FIRST, before anything else
  const authHeader = request.headers.get('Authorization')
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  // Verify the token
  const token = authHeader.split(' ')[1]
  // ... verify token and get user
  
  // Only then proceed with the business logic
}

Issue 5: No rate limiting on login

What it is: Your login endpoint accepts unlimited password attempts, making brute-force attacks easy.

Without rate limiting, an attacker can automate thousands of password guesses per minute. Any account with a weak or common password is vulnerable.

How to fix it: Add rate limiting to your authentication endpoints. The simplest approach for a Bolt app is to use an edge middleware or a simple in-memory rate limiter:

// Simple in-memory rate limiter (use Redis for production scale)
const attempts = new Map<string, { count: number; resetAt: number }>()

function checkRateLimit(ip: string): boolean {
  const now = Date.now()
  const record = attempts.get(ip)
  
  if (!record || record.resetAt < now) {
    attempts.set(ip, { count: 1, resetAt: now + 60_000 }) // 1-minute window
    return true
  }
  
  if (record.count >= 5) return false // 5 attempts per minute
  
  record.count++
  return true
}

Alternatively, use a service like Upstash (which works well with edge functions) for production-grade rate limiting without managing state yourself.


Issue 6: Verbose error messages in production

What it is: Your app returning full error details — stack traces, database errors, file paths — to users.

When Bolt generates error handling, it often uses patterns like:

try {
  // ... database operation
} catch (error) {
  return Response.json({ error: error.message }, { status: 500 })
}

In development, seeing the full error message is useful. In production, returning SQLITE_ERROR: no such table: users_v2 tells an attacker exactly what they need to know about your database structure.

How to fix it: Log errors server-side, but return a generic message to clients:

try {
  // ... database operation
} catch (error) {
  console.error('Database error:', error) // Log for your debugging
  return Response.json({ error: 'Something went wrong. Please try again.' }, { status: 500 })
}

Before your next Bolt deployment

Before you deploy your Bolt app to real users, run through this checklist:

  1. ✅ CORS locked to your specific domain (not *)
  2. ✅ All third-party API keys moved to environment variables on the server
  3. ✅ Security headers added to your deployment configuration
  4. ✅ Every API route verified to check authentication before returning data
  5. ✅ Login endpoint has rate limiting
  6. ✅ Error messages return generic text to users, not stack traces

After deploying, run VibeScan to verify your headers and CORS policy are live. The scan takes under 60 seconds and confirms what's actually visible to the outside world.

The goal isn't perfect security — it's raising the bar high enough that attackers move on to easier targets. These six fixes accomplish that for most Bolt apps.

Scan your Bolt app free →