Securing a Production Next.js SaaS: Defending Against SSRF and Stripe Webhook Attacks
March 2026 · Damir Andrijanic · 6 min read
When your product asks users to "paste any URL to scan," you are opening your server's front door to the internet.
While building ComplianceRadar, I realized quickly that raw URL input is not a convenience feature. It is a serious security boundary.
The platform runs on Next.js App Router, serverless API routes, Stripe, and AI-based analysis pipelines. Because it accepts arbitrary URLs, security had to be a first-class architectural decision from day one.
SSRF risk control
Treat every submitted URL as hostile input until proven safe.
Outbound request guardrails
Apply protocol checks, IP filtering, timeouts, and payload limits.
Payment event integrity
Verify Stripe signatures on raw payloads before unlocking features.
Production stack context
- Next.js (App Router)
- Serverless API routes
- Stripe for payments
- AI-based analysis pipelines
1) The SSRF Threat: Never trust input URLs
Server-Side Request Forgery (SSRF) happens when an attacker tricks your backend into requesting internal or restricted network resources.
Instead of a normal target URL, they submit local or metadata endpoints and try to turn your server into their private proxy.
http://localhost:3000/api/admin-delete-allhttp://169.254.169.254/latest/meta-data/
If your backend fetches whatever it receives, your external firewall is no longer your real perimeter.
To prevent that, I implemented a strict validation and sanitization pipeline before executing any outbound request.
Protocol allowlist
- Allow only http:// and https://
- Reject file://, ftp://, gopher://, data://
- Fail fast before any network call
Private IP and restricted range blocking
Before fetching, the backend resolves the hostname and rejects requests targeting private or sensitive ranges:
- 127.0.0.0/8
- 10.0.0.0/8
- 192.168.0.0/16 and 172.16.0.0/12
- 169.254.0.0/16 (metadata endpoints)
- Any localhost-equivalent internal destination
Timeouts and payload caps
Every outbound request runs with a 10-second AbortController timeout and payload size limits to reduce DoS abuse through slow responses or huge downloads.
2) Protecting revenue: Stripe webhook hardening
After securing scan requests, the next critical surface was the payment pipeline.
When a user pays, Stripe posts to /api/webhooks/stripe. A common mistake is trusting incoming JSON and unlocking premium features immediately.
That is insecure. Anyone can send a POST request to your endpoint. The only trusted source is a payload that passes Stripe signature verification.
stripe-signatureconst body = await req.text()
The key implementation detail in Next.js App Router is reading the raw request body with req.text() before any JSON parsing. Stripe signs the raw bytes. If you parse first, verification fails.
Reference implementation
Minimal webhook verification pattern for Next.js App Router.
import { headers } from "next/headers";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get("stripe-signature");
if (!signature) return new Response("Unauthorized", { status: 400 });
try {
stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Unauthorized", { status: 400 });
}
// Handle verified event
return new Response("ok");
}Security is architecture, not a checklist
Blocking SSRF keeps your scanning engine isolated from internal infrastructure and cloud metadata services.
Verifying Stripe signatures protects your billing logic from spoofed payment events and unauthorized feature unlocks.
Some of the most valuable code in production is not visible in the UI. It is the code that prevents expensive failures before they happen.
Threat model checklist (Yes/No)
Quick self-audit before you call your SaaS production-ready:
- 01Protocol Whitelist: Does your app strictly block all non-HTTP/HTTPS schemes (like file:// or ftp://) from user inputs?
- 02Private IP Blocking: Do you resolve user-submitted domains and block internal IP ranges (e.g., 127.x.x.x, 169.254.x.x) before fetching?
- 03Outbound Constraints: Are all server-side fetch requests wrapped in an AbortController with strict timeouts and payload size limits?
- 04Webhook Integrity: Do you verify Stripe webhook signatures using the raw request body (req.text()) before parsing JSON?
- 05Secret Management: Are your Webhook Signing Secrets and API keys safely kept on the server and never prefixed with NEXT_PUBLIC_?
If you want to see the project that drove these decisions, you can explore ComplianceRadar.
Live project: complianceradar.dev ↗
If you are building a SaaS right now, audit your URL input and webhook endpoints before scaling traffic.