# Auditing Serverless Functions for the 25 Posture Failures That Cost You Money or Get You Owned

Serverless platforms shift cost + security to the request shape. Origin allowlists, rate limits, CORS-with-credentials, SSRF, secret-in-log, fetch timeouts. 25 checks that prevent quota burn, account takeover, and stack-trace leaks across Netlify / Cloudflare Workers / Vercel / Lambda / Fly / Deno / Render / Railway / DigitalOcean.

Author: J.A. Watte
Published: April 22, 2026
Source: https://jwatte.com/blog/blog-tool-serverless-posture-audit/

---

Two things people get wrong about serverless functions:

1. **They are not "free."** They run on a metered platform and a single abusive client can burn your monthly quota in fifteen minutes. The whole free-tier-shaped infrastructure economy assumes everyone gates their endpoints. Most don't.
2. **The platform does not protect them.** A fresh Netlify Function, Cloudflare Worker, Vercel Serverless Function, or AWS Lambda has zero rate limit, zero Origin check, zero auth, the WAF in front (if any) protects against malformed bodies and known signatures, not abuse-shaped traffic that looks like real users.

The [Serverless Posture Audit tool](/tools/serverless-posture-audit/) runs 25 static-lint checks against pasted handler code (any platform, it auto-detects the runtime from import patterns and global APIs) and a live HTTP probe against an endpoint URL. Both modes feed into a single fix prompt that emits platform-native code, not generic Express middleware.

## The 25 checks, grouped by what they prevent

### Quota / cost control (4)

**No rate limit**, the single most common posture failure. Anyone POST-ing to your function in a loop drains the free-tier minute counter or runs the metered-call counter into your billing tier. The fix is a per-IP `Map` for in-container limits or a KV / Redis counter for shared-state platforms (Cloudflare Durable Objects, Workers KV, Vercel KV, Upstash, Deno KV).

**No outbound-fetch timeout**, your handler `fetch()`s an upstream that hangs. The platform kills your container at 10–30 seconds, billed at full duration. Wrap every `fetch` call in `AbortSignal.timeout(8000)`.

**Unbounded loop**, `while(true)` with no exit. Burns the entire execution budget and gets OOM-killed.

**No input-size cap**, a 50 MB JSON body is parsed, billed, and may crash the container. Reject with 413 once `Content-Length` exceeds a sane cap (100 KB for typical APIs).

### Browser / CORS abuse (4)

**No Origin / Referer allowlist**, any URL on the internet can POST to your endpoint, including sites that cloned your code and now embed your backend for free. The fix is an explicit allowlist with a 403 on mismatch, at the very top of the handler.

**CORS wildcard `*`**, fine for public read endpoints, never for anything user-scoped. The browser will refuse the response if credentials are involved, but a misconfigured proxy can swap the Origin in and any site becomes able to read the user's data.

**CORS wildcard with `Allow-Credentials: true`**, browsers refuse this combination at runtime, but middleware that strips and rewrites the wildcard can break the safety. A critical-severity finding in the audit because the failure mode is silent and complete.

**No HTTP-method allowlist**, accepts any verb. Probes for surface area (TRACE, PATCH, OPTIONS) get cheap reconnaissance for free. Validate method at line 1 of the handler.

### Secret + credential exposure (3)

**Hardcoded secret in source**, `AKIA...`, `ghp_...`, `sk-...`, `Bearer ...` patterns. Once committed, the secret is in git history forever. Move to platform env var, rotate the existing secret, install a pre-commit secret scanner.

**Secret echoed in log / body**, `console.log(process.env.STRIPE_KEY)` writes the key to your platform's log store, which is queryable by support staff and your future self. If you must log a token at all, log the first 4 chars + length.

**Verbose log of full request**, `console.log(request)` writes Authorization headers, cookies, IP, and body to logs. Log only what you need (path + method + status code).

### Code-execution risks (3)

**eval / new Function / vm.runIn**, dynamic code execution. Anything user-controlled that reaches it becomes RCE. Replace with a JSON-schema interpreter or an explicit switch.

**SSRF, unvalidated URL → fetch()**, user-supplied URL passed to `fetch()` without validation. Attackers point it at `169.254.169.254` (cloud metadata service), `file://`, or your internal network. Parse the URL, reject non-HTTPS, reject RFC1918 / 169.254.0.0/16 / localhost, optionally allowlist by domain.

**Path traversal risk**, user input reaches `readFile` / `createReadStream`. `../../etc/passwd` works unless you block it. Resolve to an absolute path inside a known root and reject anything outside.

### Information disclosure (3)

**Stack-trace leaked in response**, returning `error.stack` reveals file paths, function names, library versions. Catch, log server-side with a correlation ID, return `{error: "internal"}` to the client.

**Error handler returns raw Error object**, non-serialized `Error` objects toString to `[object Error]` in some runtimes and leak internal fields in others. Always shape the response.

**HTML response without CSP**, any reflected input becomes an XSS vector without a `Content-Security-Policy` header.

### Auth / replay (4)

**Mutating endpoint with no HMAC / token verify**. Origin-only checks are spoofable. Add a signed token (HMAC, JWT) tied to timestamp + path; reject stale tokens.

**DELETE / PUT with no auth check**, destructive verbs with only Origin gate are trivially replayable.

**Admin path with no auth gate**, `/admin`, `/debug`, `/internal`, `/dev`, `/.env`, `/phpmyadmin` patterns with no header check. If routing is the only gate, anyone who finds the URL gets in.

**Math.random() for token / ID / nonce**, V8's `Math.random()` is predictable enough to brute-force for security-sensitive values. Use `crypto.randomUUID()` or `crypto.getRandomValues()`.

### Caching + response hygiene (4)

**No Cache-Control on private response**, CDNs may cache user-scoped JSON. The next user sees the previous user's data. Always add `Cache-Control: private, no-store` to anything tied to a session, user, or auth token.

**429 / 503 with no Retry-After**, well-behaved clients use Retry-After to back off. Without it, they hammer you.

**Missing Content-Type on response**, browsers sniff and may mis-render HTML as text or vice versa. Always set `content-type`.

**localStorage / sessionStorage in handler**, these browser APIs don't exist in Workers / Lambda / Deno Deploy. Runtime error on first call. Use Workers KV / Deno KV / Redis / your DB instead.

## Platform-native fix hints

Once the audit detects which platform your function targets, the fix prompt switches to platform idioms:

- **Cloudflare Workers**. Durable Objects or Workers KV for cross-isolate rate limit; Turnstile for human-gated endpoints.
- **Netlify Functions**, shared `_guard.mjs` module imported by every function; Edge Functions for the upstream gate.
- **Vercel**. Edge Middleware for centralized Origin + rate limit; Vercel KV / Upstash for shared state.
- **Next.js Middleware**, `@upstash/ratelimit` with KV; runs before every matched route.
- **Fly.io**, multi-machine deploys need Redis or LiteFS; `fly-client-ip` is trustworthy.
- **Deno Deploy**. Deno KV for rate limit; signed cookies or Deno.jwt for per-user auth.
- **AWS Lambda**, API Gateway usage plans + WAF rules cover Origin + rate limit; validate `event.headers.origin` inside the handler too.
- **Render / Railway**, no built-in WAF; put Cloudflare in front or implement in-app with Redis.

## Live-probe mode

The probe tab sends a small number of GETs (baseline + cache-bust + OPTIONS preflight) through our proxy and from your browser. It does not burst, flood, or send malformed bodies, it's safe to run on production endpoints you own. It checks:

- **Double-CDN detection**, `cf-ray` AND `x-nf-request-id` / `x-vercel-id` indicates layered CDNs, which complicates cache coherence and IP trust.
- **Cache leak**, does the same URL with different Origin headers return the same cached body? If yes, your CDN is keying only on URL, not headers.
- **Permissive CORS preflight**, does an OPTIONS with a random Origin get `Access-Control-Allow-Origin: <random>` echoed back? That's a wildcard in disguise.
- **Headers-leak surface**, does the response include `Server: nginx/1.21.4` or platform-specific request IDs that leak infrastructure detail?

## When to run this

- Before publishing any new serverless function.
- After any change to a function's request handling (routes, auth, validation).
- Quarterly on every endpoint, even if nothing changed, the platform's defaults shift.
- Before integrating a third-party SDK that wraps `fetch` (Stripe, Firebase, OpenAI), your timeouts and Origin handling change.

## Related tools

- [Mega Security Analyzer](/tools/mega-security-analyzer/), TLS, headers, and CSP at the site level (the layer above the function).
- [DNS / Email Auth Audit](/tools/dns-email-audit/), SPF / DKIM / DMARC / MX, separate audit surface.
- [Security Headers Audit](/tools/security-headers-audit/), header-only audit if you only want the surface check.
- [WordPress Security Audit](/tools/wordpress-security-audit/), WP-specific exposure surface (different platform, similar mindset).

## Fact-check notes and sources

- AWS instance metadata service IP `169.254.169.254`: [AWS IMDSv2 documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html).
- Cloudflare Durable Objects rate-limit pattern: [Cloudflare Workers rate-limiting docs](https://developers.cloudflare.com/workers/examples/rate-limiting/).
- Vercel Edge Middleware: [Vercel Edge Middleware docs](https://vercel.com/docs/functions/edge-middleware).
- AbortSignal.timeout availability across runtimes: [MDN AbortSignal.timeout](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static).
- Math.random() predictability: [V8 issue 7790. Math.random() PRNG](https://v8.dev/blog/math-random).
- Web Crypto getRandomValues: [MDN Crypto.getRandomValues()](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues).
- Netlify Edge Functions runtime (Deno): [Netlify Edge Functions overview](https://docs.netlify.com/edge-functions/overview/).

*This post is informational, not security-consulting advice. Always test on staging before applying to production. Names of platforms (Cloudflare, Netlify, Vercel, AWS, Fly.io, Deno, Render, Railway, DigitalOcean) are nominative fair use.*


---

Canonical HTML: https://jwatte.com/blog/blog-tool-serverless-posture-audit/
RSS: https://jwatte.com/feed.xml
JSON Feed: https://jwatte.com/feed.json
Hero image: https://jwatte.com/images/blog-tool-serverless-posture-audit.webp
