Implementing rate limiting, SSRF protection, input validation, and other security best practices in SpaceZero edge functions.
SpaceZero includes multiple layers of security for edge functions.
// functions/lib/rateLimit.ts
interface RateLimitOptions {
maxRequests?: number;
windowMs?: number;
}
// Simple in-memory store — per-isolate, resets on cold start.
// For distributed persistence, swap in Redis or D1.
const store = new Map<string, { count: number; resetAt: number }>();
const DEFAULT_MAX_REQUESTS = 60;
const DEFAULT_WINDOW_MS = 60_000;
function getKey(context: { request: Request }): string {
const ip =
context.request.headers.get('CF-Connecting-IP') ||
context.request.headers.get('X-Forwarded-For') ||
'unknown';
return ip;
}
// functions/api/auth/check-user.ts — 10 requests / 60s
const limitResult = rateLimit({ request }, { maxRequests: 10, windowMs: 60_000 });
if (limitResult.limited) return limitResult.response!;
// functions/api/auth/send-email.ts — 5 requests / 60s
const limitResult = rateLimit(context, { maxRequests: 5, windowMs: 60_000 });
// functions/api/newsletter.ts — POST subscribe: 10 / 60s, GET unsubscribe: 20 / 60s
const limitResult = rateLimit(context, { maxRequests: 10, windowMs: 60_000 });
// functions/lib/security.ts
export function validateUrlForSSRFSafety(targetUrl: string): {
valid: boolean;
reason?: string;
sanitizedUrl?: string;
}
// Blocks: non-http(s) protocols, embedded credentials,
// localhost/.local/.internal hostnames, and private/loopback/
// link-local/metadata IPs via isBlockedIPv4() and isBlockedIPv6().
const { valid, reason } = validateUrlForSSRFSafety(body.url);
if (!valid) {
return createErrorResponse(400, reason ?? 'URL not allowed');
}
// functions/lib/validation.ts
const { data: body, error } = await validateJsonBody<{ email: string }>(request, {
email: { type: 'string', required: true },
});
if (error || !body) return createErrorResponse(400, 'Invalid request body');
export function validateEmail(email: string): boolean {
const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return pattern.test(email);
}
export function createErrorResponse(status: number, message: string, details?: unknown) {
return new Response(JSON.stringify({ error: message, details }), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
const secret =
request.headers.get('x-gumroad-signature') ||
(body.custom_fields?.['secret'] as string) ||
(body.secret as string);
if (secret !== env.GUMROAD_WEBHOOK_SECRET) {
return createErrorResponse(401, 'Unauthorized signature');
}
SpaceZero functions are same-origin — the SPA and the API share one Cloudflare Pages deployment, so no CORS layer is included. If you split the API onto its own origin, add per-route Access-Control-Allow-* headers (and handle OPTIONS preflight) yourself.