Understand resource exhaustion and denial of service vulnerabilities in AI code including unbounded loops, missing rate limits, and uncontrolled resource consumption...
Research from Databricks highlights:
"Vibe coding often produces functionally correct but resource-inefficient code that can be exploited for denial of service attacks."
The AI's focus on functionality over performance creates multiple attack vectors.
AI generates code that works perfectly for normal use but has no limits on resource consumption. This creates two major risks:
// Prompt: "Create image processing endpoint"
app.post('/process-image', async (req, res) => {
const { imageUrl, operations } = req.body;
// β VULNERABLE: No size or quantity limits
const imageBuffer = await downloadImage(imageUrl);
let processedImage = imageBuffer;
// β VULNERABLE: Unbounded loop
for (const operation of operations) {
processedImage = await applyOperation(processedImage, operation);
}
res.send(processedImage);
});
// Attack: Send huge image or hundreds of operations
// Result: Server memory exhaustion and crash
1. No Image Size Limit:
const imageBuffer = await downloadImage(imageUrl);
Attack:
2. No Operation Count Limit:
for (const operation of operations) {
processedImage = await applyOperation(processedImage, operation);
}
Attack:
3. No Rate Limiting:
app.post('/process-image', async (req, res) => {
Attack:
4. No Timeout:
5. No Validation:
One startup built a "summarize any article" AI feature without rate limiting:
Timeline:
What went wrong:
const rateLimit = require('express-rate-limit');
const sharp = require('sharp');
// β
SECURE: Rate limiting
const imageLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 requests per window
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// β
SECURE: Request size limiting
app.use(express.json({ limit: '1mb' }));
// β
SECURE: Resource limits configuration
const LIMITS = {
MAX_IMAGE_SIZE: 10 * 1024 * 1024, // 10MB
MAX_IMAGE_DIMENSION: 4000, // pixels
MAX_OPERATIONS: 5,
DOWNLOAD_TIMEOUT: 5000, // 5 seconds
PROCESSING_TIMEOUT: 30000, // 30 seconds
MAX_CONCURRENT_JOBS: 3
};
// β
SECURE: Job queue for controlled concurrency
const Queue = require('bull');
const imageQueue = new Queue('image-processing', {
redis: {
port: 6379,
host: '127.0.0.1',
},
defaultJobOptions: {
timeout: LIMITS.PROCESSING_TIMEOUT,
attempts: 2,
removeOnComplete: true,
removeOnFail: true
}
});
// β
SECURE: Controlled image download
async function downloadImageSecure(url, limits) {
// Validate URL
const urlPattern = /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b/;
if (!urlPattern.test(url)) {
throw new Error('Invalid URL');
}
// β
SECURE: Prevent SSRF by checking against internal IPs
const parsed = new URL(url);
if (isInternalIP(parsed.hostname)) {
throw new Error('Access to internal resources not allowed');
}
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), limits.DOWNLOAD_TIMEOUT);
try {
const response = await fetch(url, {
signal: controller.signal,
size: limits.MAX_IMAGE_SIZE, // Limit response size
headers: {
'User-Agent': 'ImageProcessor/1.0'
}
});
clearTimeout(timeout);
// β
SECURE: Validate content type
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.startsWith('image/')) {
throw new Error('Invalid content type');
}
// β
SECURE: Check content length
const contentLength = parseInt(response.headers.get('content-length'));
if (contentLength > limits.MAX_IMAGE_SIZE) {
throw new Error('Image too large');
}
return await response.buffer();
} finally {
clearTimeout(timeout);
}
}
app.post('/process-image', imageLimiter, async (req, res) => {
const { imageUrl, operations } = req.body;
// β
SECURE: Validate operations count
if (!Array.isArray(operations) || operations.length > LIMITS.MAX_OPERATIONS) {
return res.status(400).json({
error: `Maximum ${LIMITS.MAX_OPERATIONS} operations allowed`
});
}
// β
SECURE: Queue job instead of processing directly
const job = await imageQueue.add('process', {
imageUrl,
operations,
userId: req.user?.id,
ip: req.ip
});
res.json({
jobId: job.id,
status: 'queued',
estimatedTime: await imageQueue.getJobCounts()
});
});
// β
SECURE: Process jobs with resource controls
imageQueue.process('process', LIMITS.MAX_CONCURRENT_JOBS, async (job) => {
const { imageUrl, operations } = job.data;
// Download with limits
const imageBuffer = await downloadImageSecure(imageUrl, LIMITS);
// β
SECURE: Use sharp with resource limits
let pipeline = sharp(imageBuffer, {
limitInputPixels: LIMITS.MAX_IMAGE_DIMENSION ** 2,
sequentialRead: true, // Lower memory usage
});
// Get image metadata to validate
const metadata = await pipeline.metadata();
if (metadata.width > LIMITS.MAX_IMAGE_DIMENSION ||
metadata.height > LIMITS.MAX_IMAGE_DIMENSION) {
throw new Error('Image dimensions exceed limits');
}
// β
SECURE: Apply operations with validation
for (const op of operations) {
pipeline = applyOperationSecure(pipeline, op, LIMITS);
}
// β
SECURE: Output with format restrictions
const output = await pipeline
.jpeg({ quality: 80, progressive: true })
.toBuffer();
// Store result temporarily
await storeResult(job.id, output);
return {
success: true,
resultId: job.id,
size: output.length
};
});
function applyOperationSecure(pipeline, operation, limits) {
const { type, params } = operation;
// β
SECURE: Whitelist allowed operations
const allowedOps = ['resize', 'rotate', 'blur', 'sharpen', 'grayscale'];
if (!allowedOps.includes(type)) {
throw new Error(`Operation '${type}' not allowed`);
}
switch(type) {
case 'resize':
// β
SECURE: Validate dimensions
const { width, height } = params;
if (width > limits.MAX_IMAGE_DIMENSION ||
height > limits.MAX_IMAGE_DIMENSION) {
throw new Error('Resize dimensions exceed limits');
}
return pipeline.resize(width, height, {
fit: 'inside',
withoutEnlargement: true
});
case 'rotate':
// β
SECURE: Validate angle
const angle = parseInt(params.angle);
if (isNaN(angle) || angle < -360 || angle > 360) {
throw new Error('Invalid rotation angle');
}
return pipeline.rotate(angle);
case 'blur':
// β
SECURE: Limit blur sigma
const sigma = Math.min(params.sigma || 1, 10);
return pipeline.blur(sigma);
default:
return pipeline;
}
}
AI thinks:
await downloadImage(url) βfor (op of ops) process(op) βAI doesn't think:
AI doesn't understand:
Tutorial code:
// Simple example (no limits)
app.post('/api/process', async (req, res) => {
const result = await expensiveOperation(req.body);
res.json(result);
});
AI learns: This is the pattern AI misses: Production needs limits, queues, rate limiting
Vulnerable:
// Process all items (could be millions)
for (const item of userItems) {
await processItem(item);
}
Secure:
// Limit processing
const MAX_ITEMS = 100;
if (userItems.length > MAX_ITEMS) {
throw new Error(`Maximum ${MAX_ITEMS} items allowed`);
}
for (const item of userItems.slice(0, MAX_ITEMS)) {
await processItem(item);
}
Vulnerable:
app.post('/upload', async (req, res) => {
const file = req.file; // No size check
await processFile(file);
});
Secure:
app.post('/upload', upload.single('file'), async (req, res) => {
const file = req.file;
// Check size
if (file.size > 10 * 1024 * 1024) { // 10MB
return res.status(413).json({ error: 'File too large' });
}
// Check type
if (!['image/jpeg', 'image/png'].includes(file.mimetype)) {
return res.status(400).json({ error: 'Invalid file type' });
}
await processFile(file);
});
Vulnerable:
// No limits on expensive OpenAI calls
app.post('/summarize', async (req, res) => {
const { text } = req.body;
const summary = await openai.chat.completions.create({
model: 'gpt-4', // Expensive!
messages: [{ role: 'user', content: text }]
});
res.json({ summary });
});
Secure:
import { withRateLimit } from '@/lib/withRateLimit';
import { auth } from '@clerk/nextjs/server';
async function summarizeHandler(req: NextRequest) {
// Require authentication
const { userId } = await auth();
if (!userId) return handleUnauthorizedError();
const { text } = await req.json();
// Limit text length
if (text.length > 10000) {
return NextResponse.json(
{ error: 'Text too long (max 10,000 characters)' },
{ status: 400 }
);
}
// Track usage per user
const usage = await getUserUsage(userId);
if (usage.summarizations >= DAILY_LIMIT) {
return NextResponse.json(
{ error: 'Daily limit reached' },
{ status: 429 }
);
}
// Make API call with timeout
const summary = await Promise.race([
openai.chat.completions.create({
model: 'gpt-4-turbo', // Cheaper model
messages: [{ role: 'user', content: text }],
max_tokens: 150 // Limit response
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 30000)
)
]);
// Track usage
await incrementUserUsage(userId, 'summarizations');
return NextResponse.json({ summary });
}
export const POST = withRateLimit(summarizeHandler);
Vulnerable:
app.get('/api/users', async (req, res) => {
// β Returns ALL users (could be millions)
const users = await db.users.find();
res.json(users);
});
Secure:
app.get('/api/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
// Validate pagination
const pageNum = Math.max(1, parseInt(page));
const limitNum = Math.min(100, Math.max(1, parseInt(limit)));
// Paginated query
const users = await db.users
.find()
.skip((pageNum - 1) * limitNum)
.limit(limitNum);
const total = await db.users.countDocuments();
res.json({
users,
pagination: {
page: pageNum,
limit: limitNum,
total,
pages: Math.ceil(total / limitNum)
}
});
});
Vulnerable:
// Process nested comments (unlimited depth)
function renderComments(comment) {
let html = `<div>${comment.text}</div>`;
// β No depth limit - stack overflow possible
if (comment.replies) {
comment.replies.forEach(reply => {
html += renderComments(reply); // Recursive
});
}
return html;
}
Secure:
function renderComments(comment, depth = 0) {
// β
Limit recursion depth
const MAX_DEPTH = 10;
if (depth > MAX_DEPTH) {
return '<div>[Maximum nesting reached]</div>';
}
let html = `<div class="comment-level-${depth}">${escapeHtml(comment.text)}</div>`;
if (comment.replies && comment.replies.length > 0) {
// Limit replies shown
const MAX_REPLIES = 50;
const replies = comment.replies.slice(0, MAX_REPLIES);
replies.forEach(reply => {
html += renderComments(reply, depth + 1);
});
if (comment.replies.length > MAX_REPLIES) {
html += `<div>... ${comment.replies.length - MAX_REPLIES} more replies</div>`;
}
}
return html;
}
OpenAI GPT-4 Pricing (example):
Attack scenario:
Real incident from rate-limiting skill:
Built a "summarize any article" AI feature without rate limiting. A malicious user scripted 10,000 requests in minutes. At AI API costs, this generated $9,600 in charges in 10 minutes. The attack ran 4 hours unnoticedβtotal cost over $200,000.
AWS/Cloud Costs:
Attack scenario:
Attack:
// Request all records repeatedly
for (let i = 0; i < 1000; i++) {
fetch('/api/users'); // Returns all users
}
Result:
import { withRateLimit } from '@/lib/withRateLimit';
export const POST = withRateLimit(handler);
Protects against:
β See rate-limiting skill for implementation details
import { validateRequest } from '@/lib/validateRequest';
import { safeLongTextSchema } from '@/lib/validation';
// safeLongTextSchema has max 5000 characters built-in
const validation = validateRequest(safeLongTextSchema, userInput);
Already configured in this project:
// Next.js automatically limits body size
// Default: 4MB
// Configure in next.config.js if needed
// Set timeout for external API calls
const response = await Promise.race([
fetch('https://external-api.com/data'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), 10000)
)
]);
import { auth } from '@clerk/nextjs/server';
export async function POST(req: NextRequest) {
// Expensive operation - require auth
const { userId } = await auth();
if (!userId) {
return handleUnauthorizedError();
}
// Track usage per user
const usage = await checkUserQuota(userId);
if (usage.exceeded) {
return NextResponse.json(
{ error: 'Quota exceeded' },
{ status: 429 }
);
}
// Proceed with expensive operation
}
# Test with large payload
curl -X POST http://localhost:3000/api/endpoint \
-H "Content-Type: application/json" \
-d "{\"text\": \"$(printf 'A%.0s' {1..100000})\"}"
# Should return 400 (input too large)
# Test with many operations
curl -X POST http://localhost:3000/api/process \
-d '{"operations": ["op1","op2",...,"op1000"]}'
# Should return 400 (too many operations)
# Send 100 concurrent requests
for i in {1..100}; do
curl http://localhost:3000/api/expensive-endpoint &
done
wait
# First 5 should succeed, rest get 429 (rate limited)
# If endpoint has 30s timeout
curl --max-time 35 http://localhost:3000/api/slow-endpoint
# Should timeout at 30s (or return earlier)
β No input size limits:
const { data } = req.body; // Could be GB of data
β Unbounded loops:
for (const item of userItems) { // Could be millions
await process(item);
}
β No rate limiting on expensive operations:
app.post('/ai-generate', async (req, res) => {
await openai.createCompletion(req.body); // Costs $ per call
});
β Synchronous processing:
app.post('/process', async (req, res) => {
const result = await heavyProcessing(); // Blocks server
res.json(result);
});
β No timeout on external calls:
const data = await fetch(url); // Could hang forever
β Validate input sizes:
if (text.length > MAX_LENGTH) throw new Error('Too long');
β Limit loops:
const items = userItems.slice(0, MAX_ITEMS);
β Rate limit expensive operations:
export const POST = withRateLimit(expensiveHandler);
β Queue background jobs:
const job = await queue.add('process', data);
res.json({ jobId: job.id });
β Set timeouts:
const response = await fetchWithTimeout(url, 10000);
When creating new endpoints, ensure:
withRateLimit)β rate-limiting skill - Prevent abuse with request limits
β input-validation skill - Validate input sizes and counts
β security-testing skill - Test resource limits
β business-logic-flaws skill - Integer overflow (related)
β awareness-overview skill - Overall AI security risks
β AI generates functionally correct but resource-unlimited code β Real cost: $200,000 in 4 hours from uncontrolled AI API abuse β No size limits on images, text, operations = DoS vulnerability β Unbounded loops with user data = memory exhaustion β No rate limiting on expensive operations = cost explosion β Solution: Rate limits, input validation, timeouts, job queues, quotas β This project: Built-in rate limiting (5 req/min), input validation (max lengths)
Remember: Resource exhaustion is silent and expensiveβcode works fine in dev, causes bankruptcy in production.
Related References:
[19] Databricks. (2025). "Performance and Security: The Hidden Cost of Vibe Coding." Technical Report.
Additional Resource:
rate-limiting skill, section "The Cost of Resource Abuse"