Browse Source

security: add SSRF protection for presigned URL fetches

Validate presigned URLs before fetching by checking:
- Protocol is https
- Hostname ends with .r2.cloudflarestorage.com

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 6 months ago
parent
commit
d9e0fbc0e9
  1. 33
      src/app/api/community-workflows/[id]/route.ts

33
src/app/api/community-workflows/[id]/route.ts

@ -5,6 +5,9 @@ const COMMUNITY_WORKFLOWS_API_URL =
process.env.COMMUNITY_WORKFLOWS_API_URL ||
"https://nodebananapro.com/api/public/community-workflows";
// Allowed hostnames for presigned URL fetches (SSRF protection)
const ALLOWED_R2_HOSTS = [".r2.cloudflarestorage.com"];
interface RouteParams {
params: Promise<{ id: string }>;
}
@ -75,8 +78,34 @@ export async function GET(request: Request, { params }: RouteParams) {
);
}
// Step 2: Fetch the actual workflow from the presigned R2 URL
const workflowResponse = await fetch(urlData.downloadUrl, {
// Step 2: Validate presigned URL to prevent SSRF
let downloadUrl: URL;
try {
downloadUrl = new URL(urlData.downloadUrl);
} catch {
clearTimeout(timeoutId);
console.error("Invalid download URL received:", urlData.downloadUrl);
return NextResponse.json(
{ success: false, error: "Invalid download URL" },
{ status: 500 }
);
}
const isAllowedHost =
downloadUrl.protocol === "https:" &&
ALLOWED_R2_HOSTS.some((host) => downloadUrl.hostname.endsWith(host));
if (!isAllowedHost) {
clearTimeout(timeoutId);
console.error("Untrusted download URL hostname:", downloadUrl.hostname);
return NextResponse.json(
{ success: false, error: "Untrusted download URL" },
{ status: 403 }
);
}
// Step 3: Fetch the actual workflow from the presigned R2 URL
const workflowResponse = await fetch(downloadUrl.href, {
signal: controller.signal,
});

Loading…
Cancel
Save