Skip to main content
Published
SSRFSecurityNext.js

SSRF from frontend features

When URL inputs, previews, and proxies turn your server into an internal scanner, and how to allowlist safely in Next.js actions.

Server-side request forgery is a server bug with a frontend face. The UI lets a user supply a URL. The server fetches it. Suddenly your backend is probing cloud metadata, internal admin panels, or localhost services the browser could never reach.

Frontend features that invite SSRF: link previews, import from URL, screenshot services, webhook testers, image proxies, OG scrapers in Next.js Route Handlers.

Frontend trigger, server damage

The React form looks innocent. The danger is the server trust boundary behind it. Allowlist hosts, lock the scheme, reject credentials in the URL, and do not follow redirects to an unchecked location.

  • Allowlist hosts and schemes.
  • Block localhost, link-local, and cloud metadata names.
  • Disable or strictly limit redirects.
  • Set timeouts. Reject IP literals unless you intentionally resolve and check ranges.
app/actions/preview.tstsx
"use server";
 
// Dangerous pattern
export async function previewLink(url: string) {
  const res = await fetch(url); // may hit 169.254.169.254 or http://localhost
  return res.text();
}
 
const ALLOWED_HOSTS = new Set(["cdn.example.com", "images.example.com"]);
const BLOCKED_HOSTS = new Set([
  "localhost",
  "metadata.google.internal",
  "metadata.azure.com",
]);
 
function assertSafeUrl(raw: string) {
  let parsed: URL;
  try {
    parsed = new URL(raw);
  } catch {
    throw new Error("Invalid URL");
  }
 
  if (parsed.protocol !== "https:") throw new Error("HTTPS only");
  if (parsed.username || parsed.password) {
    throw new Error("Credentials in URL are not allowed");
  }
  if (BLOCKED_HOSTS.has(parsed.hostname.toLowerCase())) {
    throw new Error("Host not allowed");
  }
  if (!ALLOWED_HOSTS.has(parsed.hostname.toLowerCase())) {
    throw new Error("Host not allowed");
  }
  // Block literal IPs; prefer hostnames you control
  if (/\d{1,3}(\.\d{1,3}){3}$/.test(parsed.hostname) || parsed.hostname.includes(":")) {
    throw new Error("IP literals are not allowed");
  }
 
  return parsed;
}
 
export async function safePreviewLink(url: string) {
  const parsed = assertSafeUrl(url);
  const res = await fetch(parsed.toString(), {
    redirect: "error",
    signal: AbortSignal.timeout(_000),
  });
  if (!res.ok) throw new Error("Upstream failed");
  return res.text();
}
Do not fetch arbitrary user URLs from the server without controls.

More writing

Availability

Open to opportunities

Open to senior product engineer roles with real ownership across the stack. Security-minded by default.

UK-based · Remote / hybrid · Permanent or contract