
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:
-
Single quote test: Add a single quote to inputs to see if it breaks the query
- Example:
search=product'
- Example:
-
Boolean-based tests: Use logical operators to test for vulnerabilities
- Example:
search=product' OR '1'='1
- Example:
search=product' AND '1'='2
- Example:
-
UNION-based tests: Attempt to join another query to extract data
- Example:
search=product' UNION SELECT 1,2,3,4,5 --
- Example:
-
Time-based tests: Use time delays to detect blind SQL injection
- Example:
search=product' AND (SELECT SLEEP(5)) --
- Example:
Automated Testing Tools
Several tools can help identify SQL injection vulnerabilities:
-
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
-
OWASP ZAP: An integrated penetration testing tool
- Use the automated scanner
- Use the active scan specifically for SQL injection
-
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:
-
Static Application Security Testing (SAST):
- Use tools like SonarQube, ESLint with security plugins, or Semgrep
- Configure rules to detect string concatenation in SQL queries
-
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
-
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:
-
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); }); });
-
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:
-
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); }); });
-
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:
-
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' }); } });
-
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:
- Always use parameterized queries instead of string concatenation
- Prefer ORMs or query builders that handle parameterization automatically
- Validate and sanitize all inputs before using them in database operations
- Implement proper error handling to prevent information leakage
- Use prepared statements for dynamic queries with validated column names
- Apply the principle of least privilege for database access
- 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
- OWASP SQL Injection Prevention Cheat Sheet
- PortSwigger SQL Injection Tutorial
- SQLmap Documentation
- OWASP ZAP Documentation
- Prisma Security Documentation
- Sequelize Security Best Practices
References
- OWASP. (2023). “Top 10 Web Application Security Risks.” Retrieved from https://owasp.org/Top10/
- 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
- Checkmarx. (2025). “Security in Vibe Coding: Innovation Meets Risk.” Retrieved from https://checkmarx.com/blog/security-in-vibe-coding/
- Segura, T. (2025). “A Vibe Coding Security Playbook.” Infisical. Retrieved from https://infisical.com/blog/vibe-coding-security-playbook
- Trotta, F. (2025). “5 Vibe Coding Risks and Ways to Avoid Them in 2025.” Zencoder. Retrieved from https://zencoder.ai/blog/vibe-coding-risks