20Deployment

Serverless & Edge

Serverless & Edge Functions — Instruction 20

Coverage

Vercel Functions, AWS Lambda, Cloudflare Workers, Netlify Functions OWASP Serverless Top 10


Serverless General Checks

1. Function Timeout Configuration

// 🔴 No timeout = function runs forever (cost + DoS risk)
// 🟢 Always set explicit timeout
// vercel.json:
{
  "functions": {
    "api/**/*.js": {
      "maxDuration": 10,  // 10 seconds max
      "memory": 512       // 512MB max
    }
  }
}
// AWS Lambda: set timeout in configuration (recommended: 30s max for API)

2. Cold Start Sensitive Data in Logs

// 🔴 Logging sensitive data during initialization (visible in cold start logs)
const DB_URL = process.env.DATABASE_URL
console.log('Connecting to:', DB_URL)  // URL with credentials in logs!

// 🟢 Never log connection strings or secrets
console.log('Connecting to database...')  // generic message only

3. Event/Request Injection

// 🔴 Trusting event data without validation
// Lambda:
exports.handler = async (event) => {
  const userId = event.pathParameters.userId
  const data = await db.query(`SELECT * FROM users WHERE id = ${userId}`)
  // SQL injection if userId is "1 OR 1=1"!
}

// 🟢 Validate all event inputs
exports.handler = async (event) => {
  const userId = parseInt(event.pathParameters?.userId)
  if (!userId || isNaN(userId)) return { statusCode: 400, body: 'Invalid ID' }
  const data = await db.query('SELECT * FROM users WHERE id = $1', [userId])
}

4. Shared Execution Context (Warm Container)

// 🔴 CRITICAL — Variables at module scope persist between invocations
// In the SAME container/instance serving multiple requests!

let userCache = {}  // 🔴 Shared between ALL requests on this container

exports.handler = async (event) => {
  userCache[event.userId] = sensitiveData  // LEAKS to next request!
}

// 🟢 Use request-scoped variables only
exports.handler = async (event) => {
  const requestCache = {}  // new object per invocation
  // ... use requestCache locally
}

Vercel Functions

5. Middleware Security

// middleware.ts — applies to ALL routes
import { NextResponse } from 'next/server'
export function middleware(request) {
  // Add security headers to all responses
  const response = NextResponse.next()
  response.headers.set('X-Content-Type-Options', 'nosniff')
  response.headers.set('X-Frame-Options', 'DENY')
  return response
}

6. Edge Runtime Limitations

// Edge runtime has restrictions:
// 🔴 Can't use Node.js crypto module (use Web Crypto API)
// 🔴 Can't use fs module (no file system)
// 🔴 Limited npm packages

// 🟢 Edge-compatible crypto
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'])

AWS Lambda

7. IAM Role — Least Privilege

// 🔴 Lambda function with AdministratorAccess
// 🟢 Only the permissions the function actually needs
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "dynamodb:GetItem",
      "dynamodb:PutItem"
    ],
    "Resource": "arn:aws:dynamodb:us-east-1:*:table/MyTable"
  }]
}

8. Lambda Environment Variables

// 🔴 Secrets as plain env vars are stored in Lambda config (somewhat exposed)
// 🟢 Use AWS Secrets Manager or Parameter Store for sensitive values
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"
const client = new SecretsManagerClient()
const secret = await client.send(new GetSecretValueCommand({ SecretId: 'MySecret' }))

9. Function URL / API Gateway Security

// 🔴 Lambda function URL with no auth
// 🟢 Require auth:
// - API Gateway: use authorizer (JWT, Cognito, Lambda authorizer)
// - Lambda URL: set authType to 'AWS_IAM' if internal
// - Always: validate the calling identity

Cloudflare Workers

10. Wrangler Config Security

# wrangler.toml
[vars]
# 🔴 Never put secrets here
API_KEY = "secret"  # committed to git!

# 🟢 Use secrets via CLI
# wrangler secret put API_KEY

11. KV and Durable Objects Access Control

// 🔴 KV/DO without user-level access control
// 🟢 Always scope data by user
const userData = await env.KV.get(`user:${userId}:data`)
// Never: await env.KV.get('all-users-data')
// Never: allow one user to construct keys for another user's data

Netlify Functions

12. Function Authentication

// netlify/functions/secure-api.js
exports.handler = async (event) => {
  // 🔴 No auth check
  // 🟢 Verify Netlify Identity token or custom JWT
  const token = event.headers.authorization?.replace('Bearer ', '')
  if (!token) return { statusCode: 401, body: 'Unauthorized' }
  const user = verifyToken(token)
  if (!user) return { statusCode: 401, body: 'Invalid token' }
}

General Serverless Best Practices

13. No Persistent Connections Without Pooling

// 🔴 Creating new DB connection per invocation (cold start cost + connection exhaustion)
exports.handler = async (event) => {
  const db = await createConnection()  // new connection every time!
  const result = await db.query(...)
  await db.end()
}

// 🟢 Use connection pooling or PgBouncer
// Or use serverless-friendly DBs: PlanetScale, Neon, Turso, Supabase

14. Serverless Injection via Environment

// 🔴 User input used to access environment variables
const envVar = process.env[req.query.key]  // arbitrary env var access!
// Attacker: key=AWS_SECRET_ACCESS_KEY → returns your credentials

// 🟢 Never use user input to access env vars