Summary
Both deploy routes (/api/deploy/vercel and /api/deploy/netlify) parse a files field from the request body and iterate over it with no validation of array length or per-entry content size. Sending an array with thousands of large entries exhausts Node.js heap memory before any data reaches Vercel or Netlify.
The upload-zip route in the same codebase correctly applies MAX_TOTAL_UNCOMPRESSED_SIZE = 10 MB and MAX_SINGLE_FILE_SIZE = 500 KB. The deploy routes have no equivalent guard.
Affected Files
app/api/deploy/vercel/route.ts
app/api/deploy/netlify/route.ts
Root Cause
// vercel/route.ts
const { files, name, userApiKey } = await req.json();
if (!files || !Array.isArray(files)) {
return NextResponse.json({ error: "No files provided" }, { status: 400 });
}
// No length check, no per-file size check
const flatFiles = files.map(f => ({
file: f.path,
data: f.content // f.content can be arbitrarily large
}));
// netlify/route.ts
files.forEach((f: any) => {
zip.file(f.path, f.content); // same pattern, no size guard
});
Attack Scenario
An authenticated user sends:
{
"files": [
{ "path": "index.html", "content": "<1MB string>" },
... repeated 500 times ...
]
}
500 entries x 1 MB each = 500 MB held in memory while files.map(...) and zip.generateAsync(...) run. On a typical serverless instance (256-512 MB) this kills the function mid-request. Repeated calls keep the function restarting, degrading the deploy feature for all users.
Fix
Add a count cap and a per-entry content size cap before the mapping step. Reuse the constants already defined in upload-zip:
const MAX_FILE_COUNT = 500;
const MAX_ENTRY_SIZE = 500_000; // 500 KB per file
const MAX_TOTAL_SIZE = 10 * 1024 * 1024; // 10 MB aggregate
if (files.length > MAX_FILE_COUNT) {
return NextResponse.json({ error: `Too many files. Maximum is ${MAX_FILE_COUNT}.` }, { status: 413 });
}
let totalSize = 0;
for (const f of files) {
if (typeof f.content !== "string") {
return NextResponse.json({ error: "Each file entry must have a string content field." }, { status: 400 });
}
const entrySize = Buffer.byteLength(f.content, "utf8");
if (entrySize > MAX_ENTRY_SIZE) {
return NextResponse.json({ error: `File ${f.path} exceeds the 500 KB per-file limit.` }, { status: 413 });
}
totalSize += entrySize;
if (totalSize > MAX_TOTAL_SIZE) {
return NextResponse.json({ error: "Total payload exceeds the 10 MB limit." }, { status: 413 });
}
}
Apply the same block in both /vercel/route.ts and /netlify/route.ts.
GSSoC'26 contribution
Summary
Both deploy routes (
/api/deploy/verceland/api/deploy/netlify) parse afilesfield from the request body and iterate over it with no validation of array length or per-entry content size. Sending an array with thousands of large entries exhausts Node.js heap memory before any data reaches Vercel or Netlify.The
upload-ziproute in the same codebase correctly appliesMAX_TOTAL_UNCOMPRESSED_SIZE = 10 MBandMAX_SINGLE_FILE_SIZE = 500 KB. The deploy routes have no equivalent guard.Affected Files
app/api/deploy/vercel/route.tsapp/api/deploy/netlify/route.tsRoot Cause
Attack Scenario
An authenticated user sends:
{ "files": [ { "path": "index.html", "content": "<1MB string>" }, ... repeated 500 times ... ] }500 entries x 1 MB each = 500 MB held in memory while
files.map(...)andzip.generateAsync(...)run. On a typical serverless instance (256-512 MB) this kills the function mid-request. Repeated calls keep the function restarting, degrading the deploy feature for all users.Fix
Add a count cap and a per-entry content size cap before the mapping step. Reuse the constants already defined in
upload-zip:Apply the same block in both
/vercel/route.tsand/netlify/route.ts.GSSoC'26 contribution