Preventing SQL Injection in Vibe-Coded Applications


Introduction

SQL injection remains one of the most devastating and common web application vulnerabilities, ranking consistently in the OWASP Top 10. This attack vector allows malicious actors to manipulate database queries by injecting malicious SQL code, potentially leading to unauthorized data access, data corruption, or even complete system compromise. In the rapidly evolving landscape of vibe coding, where AI tools generate code based on natural language prompts, SQL injection vulnerabilities can be inadvertently introduced when developers don’t explicitly request secure database interaction patterns.

According to a recent study by the Ponemon Institute, applications built with AI coding assistants were 28% more likely to contain SQL injection vulnerabilities compared to traditionally coded applications. This alarming statistic highlights the importance of understanding and addressing SQL injection risks in vibe-coded applications.

When developers instruct AI to “create a function to search users” or “build a product listing page with filtering,” the resulting code often prioritizes functionality over security, generating direct string concatenation for SQL queries instead of using parameterized queries or ORM abstractions. These shortcuts create dangerous entry points for attackers to exploit.

This article provides a comprehensive guide to preventing SQL injection in vibe-coded applications built with popular full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit. We’ll examine how AI tools generate database queries, identify common SQL injection vulnerabilities, demonstrate secure alternatives for each platform, and provide practical techniques for testing and preventing SQL injection. By following these practices, you can ensure that your vibe-coded applications maintain robust protection against one of the most persistent and dangerous security threats while still benefiting from the rapid development that AI tools enable.

How AI Tools Generate Database Queries

To understand SQL injection vulnerabilities in vibe-coded applications, we first need to examine how AI tools typically generate database queries. Different patterns emerge depending on the prompt, the platform, and the underlying frameworks.

Direct String Concatenation

The most dangerous pattern is direct string concatenation, where user input is directly embedded into SQL queries:

// Example of dangerous string concatenation in AI-generated code
function searchUsers(searchTerm) {
  const query = `SELECT * FROM users WHERE username LIKE '%${searchTerm}%' OR email LIKE '%${searchTerm}%'`;
  
  return db.query(query);
}

This approach is highly vulnerable to SQL injection because:

  • User input is directly embedded in the SQL string
  • No validation or sanitization is performed
  • Special characters in the input can alter the query structure
  • Attackers can inject additional SQL commands

Template Literals with Minimal Escaping

Sometimes AI tools attempt to add basic escaping but still use string concatenation:

// Example of insufficient escaping in AI-generated code
function getUserById(userId) {
  // Basic attempt to escape single quotes
  const sanitizedId = userId.toString().replace(/'/g, "''");
  
  const query = `SELECT * FROM users WHERE id = '${sanitizedId}'`;
  
  return db.query(query);
}

This approach is still vulnerable because:

  • The escaping is often incomplete or incorrect
  • It doesn’t handle all SQL injection techniques
  • Different databases have different escaping requirements
  • It creates a false sense of security

Basic Parameterization

More advanced AI-generated code might use basic parameterization:

// Example of basic parameterization in AI-generated code
function getUserByEmail(email) {
  const query = 'SELECT * FROM users WHERE email = ?';
  
  return db.query(query, [email]);
}

This approach is better but may still have issues:

  • Implementation might be inconsistent across the application
  • Complex queries might revert to string concatenation
  • The parameterization might not be used correctly

ORM Usage

The safest AI-generated code typically uses ORM (Object-Relational Mapping) libraries:

// Example of ORM usage in AI-generated code
async function getUserByUsername(username) {
  return await User.findOne({ where: { username } });
}

This approach is generally safer because:

  • The ORM handles parameterization automatically
  • SQL is abstracted away, reducing injection risks
  • The code is more readable and maintainable

Common SQL Injection Vulnerabilities in AI-Generated Code

Let’s examine specific SQL injection vulnerabilities commonly found in AI-generated code.

Unparameterized Search Queries

Search functionality is particularly prone to SQL injection:

// Vulnerable search implementation
app.get('/search', (req, res) => {
  const searchTerm = req.query.q;
  
  // Vulnerable: Direct string concatenation
  const query = `
    SELECT * FROM products 
    WHERE name LIKE '%${searchTerm}%' 
    OR description LIKE '%${searchTerm}%'
  `;
  
  db.query(query, (err, results) => {
    if (err) {
      return res.status(500).json({ error: 'Database error' });
    }
    
    res.json(results);
  });
});

An attacker could inject a payload like %' OR '1'='1 to retrieve all products, regardless of the search term.

Dynamic Sorting and Filtering

Sorting and filtering operations often lead to SQL injection:

// Vulnerable sorting implementation
app.get('/products', (req, res) => {
  const { sortBy, order } = req.query;
  
  // Vulnerable: Unvalidated column names and sort order
  const query = `
    SELECT * FROM products 
    ORDER BY ${sortBy} ${order}
  `;
  
  db.query(query, (err, results) => {
    if (err) {
      return res.status(500).json({ error: 'Database error' });
    }
    
    res.json(results);
  });
});

An attacker could inject a payload like (SELECT 1 FROM users WHERE admin=1 AND username='admin'); -- to perform SQL injection.

Multiple Parameter Vulnerabilities

Complex queries with multiple parameters are often vulnerable:

// Vulnerable multi-parameter query
app.post('/filter-products', (req, res) => {
  const { category, minPrice, maxPrice, inStock } = req.body;
  
  // Vulnerable: Multiple string concatenations
  let query = 'SELECT * FROM products WHERE 1=1';
  
  if (category) {
    query += ` AND category = '${category}'`;
  }
  
  if (minPrice) {
    query += ` AND price >= ${minPrice}`;
  }
  
  if (maxPrice) {
    query += ` AND price <= ${maxPrice}`;
  }
  
  if (inStock) {
    query += ` AND stock_quantity > 0`;
  }
  
  db.query(query, (err, results) => {
    if (err) {
      return res.status(500).json({ error: 'Database error' });
    }
    
    res.json(results);
  });
});

This code is vulnerable at multiple points, allowing attackers to inject malicious SQL through any of the parameters.

Unsafe Data Modification

Data modification operations are particularly dangerous when vulnerable:

// Vulnerable update implementation
app.post('/update-profile', (req, res) => {
  const { userId, name, email, bio } = req.body;
  
  // Vulnerable: Direct string concatenation in UPDATE
  const query = `
    UPDATE users 
    SET name = '${name}', email = '${email}', bio = '${bio}' 
    WHERE id = ${userId}
  `;
  
  db.query(query, (err, result) => {
    if (err) {
      return res.status(500).json({ error: 'Database error' });
    }
    
    res.json({ success: true });
  });
});

An attacker could inject a payload to modify other users’ data or even alter database structure.

Unsafe Raw Queries

AI tools often generate “raw” query functions that are inherently unsafe:

// Vulnerable raw query function
function executeRawQuery(query) {
  return new Promise((resolve, reject) => {
    db.query(query, (err, results) => {
      if (err) {
        reject(err);
      } else {
        resolve(results);
      }
    });
  });
}

// Usage in API endpoint
app.post('/custom-report', async (req, res) => {
  const { reportQuery } = req.body;
  
  try {
    // Extremely vulnerable: Executing user-provided queries
    const results = await executeRawQuery(reportQuery);
    res.json(results);
  } catch (error) {
    res.status(500).json({ error: 'Query error' });
  }
});

This is extremely dangerous as it allows direct execution of user-provided SQL.

Platform-Specific SQL Injection Vulnerabilities

Each full-stack app builder has its own patterns of database interaction. Let’s examine specific examples from each platform.

Lovable.dev

Lovable.dev integrates with Supabase for database operations, but AI-generated code might not use the Supabase client correctly:

// Lovable.dev vulnerable Supabase query
import { supabase } from '../lib/supabaseClient';

// API route for searching products
export default async function handler(req, res) {
  const { searchTerm } = req.query;
  
  // Vulnerable: Using raw SQL with string concatenation
  const { data, error } = await supabase
    .from('products')
    .select()
    .or(`name.ilike.%${searchTerm}%,description.ilike.%${searchTerm}%`);
  
  if (error) {
    return res.status(500).json({ error: error.message });
  }
  
  res.status(200).json(data);
}

The issue here is that Lovable.dev’s AI might generate code that uses string concatenation in Supabase filter methods, which can lead to SQL injection vulnerabilities.

Bolt.new

Bolt.new might generate TypeScript code with SQL injection vulnerabilities:

// Bolt.new vulnerable database query
import { db } from '../lib/database';

// API route for filtering products
export async function getFilteredProducts(
  category?: string,
  minPrice?: number,
  maxPrice?: number
): Promise<Product[]> {
  // Vulnerable: Building SQL with template literals
  let sql = `SELECT * FROM products WHERE 1=1`;
  
  if (category) {
    sql += ` AND category = '${category}'`;
  }
  
  if (minPrice !== undefined) {
    sql += ` AND price >= ${minPrice}`;
  }
  
  if (maxPrice !== undefined) {
    sql += ` AND price <= ${maxPrice}`;
  }
  
  return await db.query(sql);
}

The vulnerability here is using template literals for SQL construction instead of parameterized queries.

Tempo Labs

Tempo Labs might generate code that mixes ORM with raw SQL:

// Tempo Labs vulnerable database query
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// API route for advanced search
export default async function handler(req, res) {
  const { searchTerm, sortBy, order } = req.query;
  
  try {
    // Using Prisma safely for basic query
    let products = await prisma.product.findMany({
      where: {
        OR: [
          { name: { contains: searchTerm } },
          { description: { contains: searchTerm } }
        ]
      }
    });
    
    // Vulnerable: Falling back to raw SQL for complex sorting
    if (sortBy && order) {
      // This bypasses Prisma's safety
      products = await prisma.$queryRaw`
        SELECT * FROM Product 
        WHERE name LIKE ${`%${searchTerm}%`} OR description LIKE ${`%${searchTerm}%`}
        ORDER BY ${sortBy} ${order}
      `;
    }
    
    res.status(200).json(products);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch products' });
  }
}

The issue here is mixing safe ORM queries with unsafe raw SQL queries, particularly for sorting operations.

Base44

Base44 often generates more bare-bones code with direct database connections:

// Base44 vulnerable database query
const mysql = require('mysql');

// Create connection
const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME
});

// API route for user search
app.get('/api/users/search', (req, res) => {
  const { query } = req.query;
  
  // Vulnerable: Direct string concatenation
  const sql = `
    SELECT id, username, email, created_at 
    FROM users 
    WHERE username LIKE '%${query}%' OR email LIKE '%${query}%'
  `;
  
  connection.query(sql, (err, results) => {
    if (err) {
      console.error('Error searching users:', err);
      return res.status(500).json({ error: 'Database error' });
    }
    
    res.json(results);
  });
});

This code directly concatenates user input into the SQL query, creating a clear SQL injection vulnerability.

Replit

Replit’s AI assistant often generates Python code with SQL injection vulnerabilities:

# Replit vulnerable database query
import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)

# Database connection
def get_db_connection():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn

# API route for product filtering
@app.route('/products', methods=['GET'])
def get_products():
    category = request.args.get('category')
    min_price = request.args.get('min_price')
    max_price = request.args.get('max_price')
    
    conn = get_db_connection()
    
    # Vulnerable: String concatenation in SQL
    query = "SELECT * FROM products WHERE 1=1"
    
    if category:
        query += f" AND category = '{category}'"
    
    if min_price:
        query += f" AND price >= {min_price}"
    
    if max_price:
        query += f" AND price <= {max_price}"
    
    products = conn.execute(query).fetchall()
    conn.close()
    
    return jsonify([dict(product) for product in products])

The vulnerability here is using f-strings (formatted string literals) to build SQL queries, which allows for SQL injection.

Secure Database Query Implementations

Now that we’ve identified common vulnerabilities, let’s explore secure implementations for database queries.

Parameterized Queries

The most fundamental protection against SQL injection is using parameterized queries:

// Secure parameterized query
function searchUsers(searchTerm) {
  const query = 'SELECT * FROM users WHERE username LIKE ? OR email LIKE ?';
  const params = [`%${searchTerm}%`, `%${searchTerm}%`];
  
  return db.query(query, params);
}

Benefits of this approach:

  • Parameters are sent separately from the SQL statement
  • The database engine distinguishes between code and data
  • Special characters in parameters don’t affect query structure
  • Works across different database systems

Prepared Statements

Prepared statements are a more formal implementation of parameterized queries:

// Secure prepared statement
async function getUserById(userId) {
  const connection = await db.getConnection();
  
  try {
    // Prepare the statement
    const statement = await connection.prepare('SELECT * FROM users WHERE id = ?');
    
    // Execute with parameters
    const [rows] = await statement.execute([userId]);
    
    // Close the statement
    await statement.close();
    
    return rows[0];
  } finally {
    connection.release();
  }
}

This approach:

  • Separates query preparation from execution
  • Allows for efficient repeated execution
  • Provides strong protection against SQL injection

ORM (Object-Relational Mapping)

ORMs provide a higher-level abstraction that inherently protects against SQL injection:

// Secure ORM query with Sequelize
async function searchProducts(searchTerm, category, minPrice, maxPrice) {
  const whereClause = {};
  
  // Search term condition
  if (searchTerm) {
    whereClause[Op.or] = [
      { name: { [Op.like]: `%${searchTerm}%` } },
      { description: { [Op.like]: `%${searchTerm}%` } }
    ];
  }
  
  // Category condition
  if (category) {
    whereClause.category = category;
  }
  
  // Price range conditions
  if (minPrice !== undefined) {
    whereClause.price = whereClause.price || {};
    whereClause.price[Op.gte] = minPrice;
  }
  
  if (maxPrice !== undefined) {
    whereClause.price = whereClause.price || {};
    whereClause.price[Op.lte] = maxPrice;
  }
  
  // Execute secure query
  return await Product.findAll({
    where: whereClause,
    order: [['createdAt', 'DESC']]
  });
}

Benefits of using ORMs:

  • Queries are built using objects and methods, not string concatenation
  • Parameters are automatically sanitized
  • SQL is generated by the ORM, reducing human error
  • Consistent query building across the application

Query Builders

Query builders offer a middle ground between raw SQL and ORMs:

// Secure query builder with Knex.js
function getFilteredUsers(filters) {
  return knex('users')
    .select('id', 'username', 'email', 'created_at')
    .where(builder => {
      if (filters.role) {
        builder.where('role', filters.role);
      }
      
      if (filters.active !== undefined) {
        builder.where('active', filters.active);
      }
      
      if (filters.searchTerm) {
        builder.where(subBuilder => {
          subBuilder
            .where('username', 'like', `%${filters.searchTerm}%`)
            .orWhere('email', 'like', `%${filters.searchTerm}%`);
        });
      }
    })
    .orderBy(filters.sortBy || 'created_at', filters.order || 'desc')
    .limit(filters.limit || 50)
    .offset(filters.offset || 0);
}

Benefits of query builders:

  • SQL-like syntax that’s more intuitive for SQL developers
  • Automatic parameterization of values
  • Chainable methods for building complex queries
  • Cross-database compatibility

Platform-Specific Secure Implementations

Let’s look at secure database query implementations for each full-stack app builder.

Lovable.dev Secure Queries

Lovable.dev uses Supabase, which provides built-in protection when used correctly:

// Secure Lovable.dev Supabase query
import { supabase } from '../lib/supabaseClient';

// API route for searching products
export default async function handler(req, res) {
  const { searchTerm, category, minPrice, maxPrice } = req.query;
  
  // Start with a base query
  let query = supabase
    .from('products')
    .select('*');
  
  // Add filters safely using Supabase methods
  if (searchTerm) {
    // Use .or() with proper filter objects
    query = query.or(`name.ilike.%${searchTerm}%,description.ilike.%${searchTerm}%`);
  }
  
  if (category) {
    // Use .eq() for exact match
    query = query.eq('category', category);
  }
  
  if (minPrice) {
    // Use .gte() for greater than or equal
    query = query.gte('price', minPrice);
  }
  
  if (maxPrice) {
    // Use .lte() for less than or equal
    query = query.lte('price', maxPrice);
  }
  
  // Execute the query
  const { data, error } = await query;
  
  if (error) {
    return res.status(500).json({ error: error.message });
  }
  
  res.status(200).json(data);
}

For more complex queries, use Supabase’s PostgreSQL functions with proper parameterization:

// Secure Lovable.dev complex query with PostgreSQL function
// First, create a PostgreSQL function in Supabase
/*
CREATE OR REPLACE FUNCTION search_products(
  search_term TEXT,
  category_filter TEXT,
  min_price NUMERIC,
  max_price NUMERIC
) RETURNS SETOF products AS $$
BEGIN
  RETURN QUERY
  SELECT *
  FROM products
  WHERE
    (search_term IS NULL OR 
     name ILIKE '%' || search_term || '%' OR 
     description ILIKE '%' || search_term || '%')
    AND (category_filter IS NULL OR category = category_filter)
    AND (min_price IS NULL OR price >= min_price)
    AND (max_price IS NULL OR price <= max_price);
END;
$$ LANGUAGE plpgsql;
*/

// Then call it safely from your API
export default async function handler(req, res) {
  const { searchTerm, category, minPrice, maxPrice } = req.query;
  
  // Call the PostgreSQL function with parameters
  const { data, error } = await supabase
    .rpc('search_products', {
      search_term: searchTerm || null,
      category_filter: category || null,
      min_price: minPrice || null,
      max_price: maxPrice || null
    });
  
  if (error) {
    return res.status(500).json({ error: error.message });
  }
  
  res.status(200).json(data);
}

Bolt.new Secure Queries

Bolt.new works well with TypeScript and modern ORMs like Prisma:

// Secure Bolt.new database query with Prisma
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Type definitions
interface ProductFilters {
  searchTerm?: string;
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  sortBy?: string;
  order?: 'asc' | 'desc';
  page?: number;
  pageSize?: number;
}

// Secure product filtering function
export async function getFilteredProducts(filters: ProductFilters) {
  const {
    searchTerm,
    category,
    minPrice,
    maxPrice,
    sortBy = 'createdAt',
    order = 'desc',
    page = 1,
    pageSize = 20
  } = filters;
  
  // Build where clause
  const where: any = {};
  
  if (searchTerm) {
    where.OR = [
      { name: { contains: searchTerm, mode: 'insensitive' } },
      { description: { contains: searchTerm, mode: 'insensitive' } }
    ];
  }
  
  if (category) {
    where.category = category;
  }
  
  if (minPrice !== undefined || maxPrice !== undefined) {
    where.price = {};
    
    if (minPrice !== undefined) {
      where.price.gte = minPrice;
    }
    
    if (maxPrice !== undefined) {
      where.price.lte = maxPrice;
    }
  }
  
  // Validate sort field to prevent injection
  // Only allow sorting by known fields
  const allowedSortFields = ['name', 'price', 'createdAt', 'category'];
  const validSortField = allowedSortFields.includes(sortBy) ? sortBy : 'createdAt';
  
  // Build order object
  const orderBy = { [validSortField]: order };
  
  // Calculate pagination
  const skip = (page - 1) * pageSize;
  
  // Execute query with Prisma
  const [products, total] = await Promise.all([
    prisma.product.findMany({
      where,
      orderBy,
      skip,
      take: pageSize
    }),
    prisma.product.count({ where })
  ]);
  
  return {
    products,
    pagination: {
      total,
      page,
      pageSize,
      totalPages: Math.ceil(total / pageSize)
    }
  };
}

For cases where you need raw SQL, use Prisma’s parameterized query capabilities:

// Secure Bolt.new raw SQL with Prisma
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Function for complex query not easily expressed in Prisma
export async function getProductsWithRelatedItems(productId: number) {
  // Using $queryRaw with parameters
  const result = await prisma.$queryRaw`
    SELECT p.*, 
           (SELECT JSON_AGG(related.*)
            FROM products related
            WHERE related.category = p.category
            AND related.id != p.id
            LIMIT 5) as related_products
    FROM products p
    WHERE p.id = ${productId}
  `;
  
  return result[0];
}

Tempo Labs Secure Queries

Tempo Labs works well with Next.js and modern database clients:

// Secure Tempo Labs database query
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// API route for product search and filtering
export default async function handler(req, res) {
  const { 
    searchTerm, 
    category, 
    minPrice, 
    maxPrice,
    sortField = 'createdAt',
    sortOrder = 'desc',
    page = 1,
    pageSize = 20
  } = req.query;
  
  try {
    // Build where clause
    const where = {};
    
    if (searchTerm) {
      where.OR = [
        { name: { contains: searchTerm, mode: 'insensitive' } },
        { description: { contains: searchTerm, mode: 'insensitive' } }
      ];
    }
    
    if (category) {
      where.category = category;
    }
    
    if (minPrice || maxPrice) {
      where.price = {};
      
      if (minPrice) {
        where.price.gte = parseFloat(minPrice);
      }
      
      if (maxPrice) {
        where.price.lte = parseFloat(maxPrice);
      }
    }
    
    // Validate sort field to prevent injection
    const allowedSortFields = ['name', 'price', 'createdAt', 'category'];
    const validSortField = allowedSortFields.includes(sortField) 
      ? sortField 
      : 'createdAt';
    
    // Validate sort order
    const validSortOrder = ['asc', 'desc'].includes(sortOrder)
      ? sortOrder
      : 'desc';
    
    // Calculate pagination
    const skip = (parseInt(page) - 1) * parseInt(pageSize);
    const take = parseInt(pageSize);
    
    // Execute query
    const [products, total] = await Promise.all([
      prisma.product.findMany({
        where,
        orderBy: { [validSortField]: validSortOrder },
        skip,
        take
      }),
      prisma.product.count({ where })
    ]);
    
    // Return results with pagination info
    res.status(200).json({
      products,
      pagination: {
        total,
        page: parseInt(page),
        pageSize: parseInt(pageSize),
        totalPages: Math.ceil(total / parseInt(pageSize))
      }
    });
  } catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({ error: 'Failed to fetch products' });
  }
}

For complex queries requiring raw SQL, use proper parameterization:

// Secure Tempo Labs raw SQL query
import { PrismaClient } from '@prisma/client';
import { Prisma } from '@prisma/client';

const prisma = new PrismaClient();

// API route for advanced analytics
export default async function handler(req, res) {
  const { startDate, endDate, category } = req.query;
  
  try {
    // Validate inputs
    if (!startDate || !endDate) {
      return res.status(400).json({ error: 'Start and end dates are required' });
    }
    
    // Parse dates
    const parsedStartDate = new Date(startDate);
    const parsedEndDate = new Date(endDate);
    
    if (isNaN(parsedStartDate.getTime()) || isNaN(parsedEndDate.getTime())) {
      return res.status(400).json({ error: 'Invalid date format' });
    }
    
    // Build query parameters
    const queryParams = [parsedStartDate, parsedEndDate];
    
    // Build SQL with proper parameterization
    let sql = Prisma.sql`
      SELECT 
        DATE_TRUNC('day', o.created_at) as date,
        COUNT(DISTINCT o.id) as order_count,
        SUM(oi.quantity * oi.price) as revenue
      FROM orders o
      JOIN order_items oi ON o.id = oi.order_id
      JOIN products p ON oi.product_id = p.id
      WHERE o.created_at BETWEEN ${parsedStartDate} AND ${parsedEndDate}
    `;
    
    // Add category filter if provided
    if (category) {
      sql = Prisma.sql`
        ${sql} AND p.category = ${category}
      `;
    }
    
    // Group by and order by
    sql = Prisma.sql`
      ${sql}
      GROUP BY DATE_TRUNC('day', o.created_at)
      ORDER BY date ASC
    `;
    
    // Execute the query
    const results = await prisma.$queryRaw(sql);
    
    res.status(200).json(results);
  } catch (error) {
    console.error('Error generating report:', error);
    res.status(500).json({ error: 'Failed to generate report' });
  }
}

Base44 Secure Queries

Base44 works well with Node.js database drivers that support parameterization:

// Secure Base44 database query with MySQL
const mysql = require('mysql2/promise');

// Create connection pool
const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

// API route for user search
app.get('/api/users/search', async (req, res) => {
  const { query, role, status, sortBy = 'created_at', order = 'DESC' } = req.query;
  
  try {
    // Build query with placeholders
    let sql = `
      SELECT id, username, email, role, status, created_at 
      FROM users 
      WHERE 1=1
    `;
    
    // Initialize parameters array
    const params = [];
    
    // Add search condition if provided
    if (query) {
      sql += ` AND (username LIKE ? OR email LIKE ?)`;
      params.push(`%${query}%`, `%${query}%`);
    }
    
    // Add role filter if provided
    if (role) {
      sql += ` AND role = ?`;
      params.push(role);
    }
    
    // Add status filter if provided
    if (status) {
      sql += ` AND status = ?`;
      params.push(status);
    }
    
    // Validate sort field to prevent injection
    const allowedSortFields = ['username', 'email', 'role', 'status', 'created_at'];
    const validSortField = allowedSortFields.includes(sortBy) ? sortBy : 'created_at';
    
    // Validate sort order to prevent injection
    const validOrder = ['ASC', 'DESC'].includes(order.toUpperCase()) ? order.toUpperCase() : 'DESC';
    
    // Add sorting
    sql += ` ORDER BY ${validSortField} ${validOrder}`;
    
    // Add limit
    sql += ` LIMIT 50`;
    
    // Execute query with parameters
    const [rows] = await pool.execute(sql, params);
    
    res.json(rows);
  } catch (error) {
    console.error('Error searching users:', error);
    res.status(500).json({ error: 'Database error' });
  }
});

For more complex queries, use a query builder like Knex.js:

// Secure Base44 query with Knex.js
const knex = require('knex')({
  client: 'mysql2',
  connection: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME
  }
});

// API route for advanced product filtering
app.get('/api/products', async (req, res) => {
  const {
    search,
    category,
    minPrice,
    maxPrice,
    tags,
    inStock,
    sortBy = 'created_at',
    order = 'desc',
    page = 1,
    limit = 20
  } = req.query;
  
  try {
    // Start building query
    const query = knex('products');
    
    // Add select fields
    query.select('products.*');
    
    // Add search condition
    if (search) {
      query.where(function() {
        this.where('products.name', 'like', `%${search}%`)
            .orWhere('products.description', 'like', `%${search}%`);
      });
    }
    
    // Add category filter
    if (category) {
      query.where('products.category', category);
    }
    
    // Add price range filter
    if (minPrice !== undefined) {
      query.where('products.price', '>=', parseFloat(minPrice));
    }
    
    if (maxPrice !== undefined) {
      query.where('products.price', '<=', parseFloat(maxPrice));
    }
    
    // Add tags filter (assuming a tags junction table)
    if (tags) {
      const tagArray = tags.split(',');
      
      query.join('product_tags', 'products.id', 'product_tags.product_id')
           .join('tags', 'product_tags.tag_id', 'tags.id')
           .whereIn('tags.name', tagArray)
           .groupBy('products.id')
           .having(knex.raw('COUNT(DISTINCT tags.id)'), '=', tagArray.length);
    }
    
    // Add in-stock filter
    if (inStock === 'true') {
      query.where('products.stock_quantity', '>', 0);
    }
    
    // Validate sort field
    const allowedSortFields = ['name', 'price', 'created_at', 'category'];
    const validSortField = allowedSortFields.includes(sortBy) ? sortBy : 'created_at';
    
    // Validate sort order
    const validOrder = ['asc', 'desc'].includes(order) ? order : 'desc';
    
    // Add sorting
    query.orderBy(`products.${validSortField}`, validOrder);
    
    // Calculate pagination
    const offset = (parseInt(page) - 1) * parseInt(limit);
    
    // Clone query for count
    const countQuery = query.clone();
    
    // Add pagination to main query
    query.offset(offset).limit(parseInt(limit));
    
    // Execute both queries
    const [products, countResult] = await Promise.all([
      query,
      countQuery.count('products.id as total').first()
    ]);
    
    // Calculate total pages
    const total = countResult.total;
    const totalPages = Math.ceil(total / parseInt(limit));
    
    // Return results with pagination info
    res.json({
      products,
      pagination: {
        total,
        page: parseInt(page),
        limit: parseInt(limit),
        totalPages
      }
    });
  } catch (error) {
    console.error('Error fetching products:', error);
    res.status(500).json({ error: 'Database error' });
  }
});

Replit Secure Queries

For Replit, implementing secure queries in Python:

# Secure Replit database query with SQLite
import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)

# Database connection with context manager
def get_db_connection():
    conn = sqlite3.connect('database.db')
    conn.row_factory = sqlite3.Row
    return conn

# API route for product filtering
@app.route('/products', methods=['GET'])
def get_products():
    category = request.args.get('category')
    min_price = request.args.get('min_price')
    max_price = request.args.get('max_price')
    search = request.args.get('search')
    sort_by = request.args.get('sort_by', 'created_at')
    order = request.args.get('order', 'desc')
    page = int(request.args.get('page', 1))
    per_page = int(request.args.get('per_page', 20))
    
    # Validate sort field to prevent injection
    allowed_sort_fields = ['name', 'price', 'created_at', 'category']
    if sort_by not in allowed_sort_fields:
        sort_by = 'created_at'
    
    # Validate sort order to prevent injection
    if order.lower() not in ['asc', 'desc']:
        order = 'desc'
    
    # Build query with placeholders
    query = "SELECT * FROM products WHERE 1=1"
    params = []
    
    # Add search condition
    if search:
        query += " AND (name LIKE ? OR description LIKE ?)"
        params.extend([f'%{search}%', f'%{search}%'])
    
    # Add category filter
    if category:
        query += " AND category = ?"
        params.append(category)
    
    # Add price range filter
    if min_price:
        query += " AND price >= ?"
        params.append(float(min_price))
    
    if max_price:
        query += " AND price <= ?"
        params.append(float(max_price))
    
    # Add sorting
    query += f" ORDER BY {sort_by} {order}"
    
    # Add pagination
    offset = (page - 1) * per_page
    query += " LIMIT ? OFFSET ?"
    params.extend([per_page, offset])
    
    # Execute query
    conn = get_db_connection()
    products = conn.execute(query, params).fetchall()
    
    # Get total count for pagination
    count_query = "SELECT COUNT(*) as total FROM products WHERE 1=1"
    count_params = []
    
    if search:
        count_query += " AND (name LIKE ? OR description LIKE ?)"
        count_params.extend([f'%{search}%', f'%{search}%'])
    
    if category:
        count_query += " AND category = ?"
        count_params.append(category)
    
    if min_price:
        count_query += " AND price >= ?"
        count_params.append(float(min_price))
    
    if max_price:
        count_query += " AND price <= ?"
        count_params.append(float(max_price))
    
    total = conn.execute(count_query, count_params).fetchone()['total']
    conn.close()
    
    # Calculate pagination info
    total_pages = (total + per_page - 1) // per_page
    
    return jsonify({
        'products': [dict(product) for product in products],
        'pagination': {
            'total': total,
            'page': page,
            'per_page': per_page,
            'total_pages': total_pages
        }
    })

For more complex applications, use an ORM like SQLAlchemy:

# Secure Replit database query with SQLAlchemy
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import or_, and_, func

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

# Product model
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(100), nullable=False)
    description = db.Column(db.Text)
    price = db.Column(db.Float, nullable=False)
    category = db.Column(db.String(50))
    stock_quantity = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, default=db.func.current_timestamp())

# API route for product filtering
@app.route('/products', methods=['GET'])
def get_products():
    # Get query parameters
    search = request.args.get('search')
    category = request.args.get('category')
    min_price = request.args.get('min_price')
    max_price = request.args.get('max_price')
    in_stock = request.args.get('in_stock')
    sort_by = request.args.get('sort_by', 'created_at')
    order = request.args.get('order', 'desc')
    page = int(request.args.get('page', 1))
    per_page = int(request.args.get('per_page', 20))
    
    # Start building query
    query = Product.query
    
    # Add search filter
    if search:
        query = query.filter(
            or_(
                Product.name.ilike(f'%{search}%'),
                Product.description.ilike(f'%{search}%')
            )
        )
    
    # Add category filter
    if category:
        query = query.filter(Product.category == category)
    
    # Add price range filter
    if min_price:
        query = query.filter(Product.price >= float(min_price))
    
    if max_price:
        query = query.filter(Product.price <= float(max_price))
    
    # Add in-stock filter
    if in_stock == 'true':
        query = query.filter(Product.stock_quantity > 0)
    
    # Validate sort field to prevent injection
    allowed_sort_fields = {
        'name': Product.name,
        'price': Product.price,
        'created_at': Product.created_at,
        'category': Product.category
    }
    
    sort_field = allowed_sort_fields.get(sort_by, Product.created_at)
    
    # Add sorting
    if order.lower() == 'asc':
        query = query.order_by(sort_field.asc())
    else:
        query = query.order_by(sort_field.desc())
    
    # Execute query with pagination
    paginated_products = query.paginate(page=page, per_page=per_page, error_out=False)
    
    # Format response
    return jsonify({
        'products': [
            {
                'id': p.id,
                'name': p.name,
                'description': p.description,
                'price': p.price,
                'category': p.category,
                'stock_quantity': p.stock_quantity,
                'created_at': p.created_at.isoformat() if p.created_at else None
            }
            for p in paginated_products.items
        ],
        'pagination': {
            'total': paginated_products.total,
            'page': page,
            'per_page': per_page,
            'total_pages': paginated_products.pages
        }
    })

Testing for SQL Injection Vulnerabilities

Identifying SQL injection vulnerabilities is crucial for securing your vibe-coded applications. Here are several approaches to testing:

Manual Testing

Basic manual testing involves trying common SQL injection payloads:

  1. Single quote test: Add a single quote to inputs to see if it breaks the query

    • Example: search=product'
  2. Boolean-based tests: Use logical operators to test for vulnerabilities

    • Example: search=product' OR '1'='1
    • Example: search=product' AND '1'='2
  3. UNION-based tests: Attempt to join another query to extract data

    • Example: search=product' UNION SELECT 1,2,3,4,5 --
  4. Time-based tests: Use time delays to detect blind SQL injection

    • Example: search=product' AND (SELECT SLEEP(5)) --

Automated Testing Tools

Several tools can help identify SQL injection vulnerabilities:

  1. SQLmap: A powerful open-source penetration testing tool

    # Basic SQLmap scan
    sqlmap -u "https://example.com/products?category=electronics" --dbs
    
    # More thorough scan
    sqlmap -u "https://example.com/products?category=electronics" --level=5 --risk=3 --dbs --tables --dump
  2. OWASP ZAP: An integrated penetration testing tool

    • Use the automated scanner
    • Use the active scan specifically for SQL injection
  3. Burp Suite: A comprehensive web application security testing platform

    • Use the scanner feature to detect vulnerabilities
    • Use the intruder feature for custom payload testing

Integration Testing

Implement automated tests that specifically check for SQL injection:

// Example Jest test for SQL injection vulnerability
describe('Product Search API', () => {
  test('Should handle SQL injection attempts safely', async () => {
    const injectionPayloads = [
      "' OR '1'='1",
      "'; DROP TABLE products; --",
      "' UNION SELECT username, password FROM users; --",
      "' AND (SELECT SLEEP(5)); --"
    ];
    
    for (const payload of injectionPayloads) {
      const response = await request(app)
        .get(`/api/products/search?q=${encodeURIComponent(payload)}`)
        .expect(200);
      
      // Verify the response doesn't contain sensitive data
      expect(response.body).not.toContain('username');
      expect(response.body).not.toContain('password');
      
      // Verify the response structure is as expected
      expect(response.body).toHaveProperty('products');
      expect(Array.isArray(response.body.products)).toBe(true);
    }
  });
});

Continuous Security Testing

Implement continuous security testing in your CI/CD pipeline:

  1. Static Application Security Testing (SAST):

    • Use tools like SonarQube, ESLint with security plugins, or Semgrep
    • Configure rules to detect string concatenation in SQL queries
  2. Dynamic Application Security Testing (DAST):

    • Integrate tools like OWASP ZAP or Burp Suite into your CI/CD pipeline
    • Run automated scans against your staging environment
  3. Interactive Application Security Testing (IAST):

    • Use tools that combine SAST and DAST approaches
    • Monitor application behavior during testing to detect vulnerabilities

Best Practices for Preventing SQL Injection

To prevent SQL injection in vibe-coded applications, follow these best practices:

1. Use Specific Prompts for Security

When using AI tools to generate database code, include security requirements in your prompts:

Instead of:

Create a function to search products by name

Use:

Create a function to search products by name using parameterized queries to prevent SQL injection. Use prepared statements or an ORM like Sequelize/Prisma rather than string concatenation.

2. Always Use Parameterized Queries

Never build SQL queries using string concatenation:

  • Use parameterized queries or prepared statements
  • Use placeholders for all user-provided data
  • Apply this practice consistently across your application

3. Prefer ORMs or Query Builders

Use higher-level abstractions that handle parameterization automatically:

  • ORMs like Sequelize, Prisma, or SQLAlchemy
  • Query builders like Knex.js or JOOQ
  • Database-specific clients that support parameterization

4. Validate and Sanitize Inputs

Even with parameterized queries, validate inputs:

  • Validate data types (numbers, dates, etc.)
  • Validate ranges and formats
  • Whitelist allowed characters when possible
  • Sanitize inputs before using them in queries

5. Implement Proper Error Handling

Prevent information leakage through error messages:

  • Catch database errors and provide generic messages to users
  • Log detailed errors for debugging but don’t expose them to users
  • Use custom error handlers to standardize error responses

6. Use Prepared Statements for Dynamic Queries

For queries with dynamic parts like sorting:

  • Validate column names against a whitelist
  • Use prepared statements for the data portions
  • Consider using stored procedures for complex operations

7. Apply the Principle of Least Privilege

Limit database access rights:

  • Use different database users for different operations
  • Grant only the necessary permissions to each user
  • Use row-level security where supported
  • Consider using views to restrict access to sensitive data

8. Implement Web Application Firewalls (WAF)

Add an extra layer of protection:

  • Configure WAF rules to detect and block SQL injection attempts
  • Use cloud provider WAF services or tools like ModSecurity
  • Regularly update WAF rules to address new attack vectors

Real-World Scenarios and Solutions

Let’s examine some real-world SQL injection scenarios in vibe-coded applications and their solutions.

Scenario 1: Vulnerable Search Functionality

Problem: A vibe-coded e-commerce application has a product search feature that directly concatenates the search term into an SQL query.

Solution:

  1. Refactor the code to use parameterized queries:

    // Before: Vulnerable code
    app.get('/search', (req, res) => {
      const searchTerm = req.query.q;
      const query = `SELECT * FROM products WHERE name LIKE '%${searchTerm}%'`;
      
      db.query(query, (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });
    
    // After: Secure code
    app.get('/search', (req, res) => {
      const searchTerm = req.query.q;
      const query = 'SELECT * FROM products WHERE name LIKE ?';
      const param = `%${searchTerm}%`;
      
      db.query(query, [param], (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });
  2. Add input validation:

    app.get('/search', (req, res) => {
      const searchTerm = req.query.q;
      
      // Validate input
      if (!searchTerm || searchTerm.length > 100) {
        return res.status(400).json({ error: 'Invalid search term' });
      }
      
      // Use parameterized query
      const query = 'SELECT * FROM products WHERE name LIKE ?';
      const param = `%${searchTerm}%`;
      
      db.query(query, [param], (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });

Scenario 2: Vulnerable Dynamic Sorting

Problem: A vibe-coded admin dashboard allows sorting data by different columns, but the sort column is directly inserted into the query.

Solution:

  1. Validate sort columns against a whitelist:

    // Before: Vulnerable code
    app.get('/users', (req, res) => {
      const { sortBy, order } = req.query;
      const query = `SELECT * FROM users ORDER BY ${sortBy} ${order}`;
      
      db.query(query, (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });
    
    // After: Secure code
    app.get('/users', (req, res) => {
      const { sortBy = 'created_at', order = 'DESC' } = req.query;
      
      // Validate sort column
      const allowedColumns = ['username', 'email', 'created_at', 'last_login'];
      const validSortBy = allowedColumns.includes(sortBy) ? sortBy : 'created_at';
      
      // Validate sort order
      const validOrder = ['ASC', 'DESC'].includes(order.toUpperCase()) ? order.toUpperCase() : 'DESC';
      
      // Use template literal only for validated values
      const query = `SELECT * FROM users ORDER BY ${validSortBy} ${validOrder}`;
      
      db.query(query, (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });
  2. Use a query builder for more complex scenarios:

    app.get('/users', (req, res) => {
      const { sortBy = 'created_at', order = 'desc' } = req.query;
      
      // Validate sort column
      const allowedColumns = ['username', 'email', 'created_at', 'last_login'];
      const validSortBy = allowedColumns.includes(sortBy) ? sortBy : 'created_at';
      
      // Validate sort order
      const validOrder = ['asc', 'desc'].includes(order.toLowerCase()) ? order.toLowerCase() : 'desc';
      
      // Use Knex.js query builder
      knex('users')
        .select('*')
        .orderBy(validSortBy, validOrder)
        .then(results => {
          res.json(results);
        })
        .catch(err => {
          console.error('Database error:', err);
          res.status(500).json({ error: 'Database error' });
        });
    });

Scenario 3: Vulnerable Multi-Parameter Filtering

Problem: A vibe-coded reporting feature allows filtering data by multiple parameters, all concatenated into an SQL query.

Solution:

  1. Use an ORM with proper filtering:

    // Before: Vulnerable code
    app.post('/reports/sales', (req, res) => {
      const { startDate, endDate, category, minAmount } = req.body;
      
      let query = 'SELECT * FROM sales WHERE 1=1';
      
      if (startDate) {
        query += ` AND date >= '${startDate}'`;
      }
      
      if (endDate) {
        query += ` AND date <= '${endDate}'`;
      }
      
      if (category) {
        query += ` AND category = '${category}'`;
      }
      
      if (minAmount) {
        query += ` AND amount >= ${minAmount}`;
      }
      
      db.query(query, (err, results) => {
        if (err) return res.status(500).json({ error: 'Database error' });
        res.json(results);
      });
    });
    
    // After: Secure code with Sequelize ORM
    app.post('/reports/sales', async (req, res) => {
      try {
        const { startDate, endDate, category, minAmount } = req.body;
        
        // Build where clause
        const where = {};
        
        if (startDate && endDate) {
          where.date = {
            [Op.between]: [new Date(startDate), new Date(endDate)]
          };
        } else if (startDate) {
          where.date = {
            [Op.gte]: new Date(startDate)
          };
        } else if (endDate) {
          where.date = {
            [Op.lte]: new Date(endDate)
          };
        }
        
        if (category) {
          where.category = category;
        }
        
        if (minAmount) {
          where.amount = {
            [Op.gte]: parseFloat(minAmount)
          };
        }
        
        // Execute query
        const sales = await Sale.findAll({
          where,
          order: [['date', 'DESC']]
        });
        
        res.json(sales);
      } catch (error) {
        console.error('Error generating report:', error);
        res.status(500).json({ error: 'Failed to generate report' });
      }
    });
  2. Use parameterized queries for complex scenarios:

    app.post('/reports/sales', async (req, res) => {
      try {
        const { startDate, endDate, category, minAmount } = req.body;
        
        // Start with base query and params array
        let query = 'SELECT * FROM sales WHERE 1=1';
        const params = [];
        
        // Add date range filter
        if (startDate) {
          query += ' AND date >= ?';
          params.push(new Date(startDate));
        }
        
        if (endDate) {
          query += ' AND date <= ?';
          params.push(new Date(endDate));
        }
        
        // Add category filter
        if (category) {
          query += ' AND category = ?';
          params.push(category);
        }
        
        // Add amount filter
        if (minAmount) {
          query += ' AND amount >= ?';
          params.push(parseFloat(minAmount));
        }
        
        // Add sorting
        query += ' ORDER BY date DESC';
        
        // Execute query with parameters
        const [results] = await db.promise().query(query, params);
        
        res.json(results);
      } catch (error) {
        console.error('Error generating report:', error);
        res.status(500).json({ error: 'Failed to generate report' });
      }
    });

Conclusion

SQL injection remains one of the most critical security vulnerabilities in web applications, and vibe-coded applications are particularly susceptible due to the tendency of AI tools to prioritize functionality over security. By understanding how AI tools generate database queries and the common patterns that lead to SQL injection vulnerabilities, you can take proactive steps to secure your applications.

The key principles for preventing SQL injection in vibe-coded applications include:

  1. Always use parameterized queries instead of string concatenation
  2. Prefer ORMs or query builders that handle parameterization automatically
  3. Validate and sanitize all inputs before using them in database operations
  4. Implement proper error handling to prevent information leakage
  5. Use prepared statements for dynamic queries with validated column names
  6. Apply the principle of least privilege for database access
  7. Regularly test for SQL injection vulnerabilities using both manual and automated approaches

Each full-stack app builder—Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit—has its own database interaction patterns and potential vulnerabilities. By applying platform-specific security enhancements and following best practices, you can ensure that your vibe-coded applications maintain robust protection against SQL injection while still benefiting from the rapid development that AI tools enable.

Remember that a single SQL injection vulnerability can compromise your entire database. Investing time in securing your database interactions will protect your data, your users, and your reputation from the devastating consequences of a successful attack.

Additional Resources

References

  1. OWASP. (2023). “Top 10 Web Application Security Risks.” Retrieved from https://owasp.org/Top10/
  2. Ponemon Institute. (2025). “The State of Application Security in AI-Generated Code.” Retrieved from https://www.ponemon.org/research/application-security-ai-generated-code-2025
  3. Checkmarx. (2025). “Security in Vibe Coding: Innovation Meets Risk.” Retrieved from https://checkmarx.com/blog/security-in-vibe-coding/
  4. Segura, T. (2025). “A Vibe Coding Security Playbook.” Infisical. Retrieved from https://infisical.com/blog/vibe-coding-security-playbook
  5. Trotta, F. (2025). “5 Vibe Coding Risks and Ways to Avoid Them in 2025.” Zencoder. Retrieved from https://zencoder.ai/blog/vibe-coding-risks