• Agents
  • Pricing
  • Blog
Log in
Get started

Security for apps built with AI. Paste a URL, get a report, fix what matters.

Product

  • How it works
  • What we find
  • Pricing
  • Agents
  • MCP Server
  • CLI
  • GitHub Action

Resources

  • Guides
  • Blog
  • Docs
  • OWASP Top 10
  • Glossary
  • FAQ

Security

  • Supabase Security
  • Next.js Security
  • Lovable Security
  • Cursor Security
  • Bolt Security

Legal

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Imprint
© 2026 Flowpatrol. All rights reserved.
Home/OWASP Top 10/API Top 10/API7: Server Side Request Forgery
API7CWE-918

The 'I'll fetch any URL you give me' bug
Server Side Request Forgery

You asked for a link preview feature. You shipped a way for attackers to read the cloud metadata endpoint.

The bug behind one of the largest cloud breaches ever — 100 million records at Capital One.

Reference: API Top 10 (2023) — API7·Last updated April 7, 2026·By Flowpatrol Team
Server Side Request Forgery illustration

Half the features in a modern app fetch a URL on behalf of a user. Webhook receivers, image proxies, link previews, 'import from URL' buttons, third-party integrations. Every one of them is a small polite helper the server runs. Every one of them will happily run against your own internal network unless you teach it not to.

Server Side Request Forgery is when your server makes an outbound HTTP call on behalf of a user — webhook, preview, image proxy, import-from-URL — and the user gets to choose the destination. The server is inside your network. The user is not. Suddenly the user can reach things only the server was supposed to see.

What your AI actually built

You asked for a webhook endpoint that lets users register a callback URL. Or an image proxy so avatars load through your CDN. Or a link unfurler for pasted URLs. The model gave you a tidy fetch call wrapped in a route.

What it did not give you was the allowlist. The fetch will go anywhere the DNS resolver points — including 169.254.169.254 (cloud metadata), 127.0.0.1 (whatever is bound to loopback), and 10.0.0.0/8 (your internal services). To the server, all three look like perfectly valid URLs.

The really painful version is the redirect trick. You block localhost in the handler, the model says 'great, safe,' and then attacker.com returns a 302 pointing at 169.254.169.254 and the fetch follows it. The check happened once. The network call happened twice.

How it gets exploited

A SaaS app ships a nice feature: paste any URL and we will generate a preview card.

  • 1
    Find the feature
    The attacker pastes https://example.com and watches the response — a preview with title and og:image. Clearly a server-side fetch.
  • 2
    Point it inward
    They paste http://169.254.169.254/latest/meta-data/iam/security-credentials/ — the AWS EC2 instance metadata endpoint. The preview card comes back with the name of an IAM role.
  • 3
    Drill down
    They paste the full credentials URL. The preview now contains a valid AccessKeyId, SecretAccessKey, and Token — the temporary credentials the EC2 instance uses to talk to S3.
  • 4
    Walk into the bucket
    They configure awscli with the stolen token and list buckets. The application's S3 bucket — which holds every user's uploaded document — is now readable from the attacker's laptop.
  • A link-preview feature became a full AWS credential theft. No password was cracked, no dependency was exploited, no shell was opened. The server was just too polite about which URLs it would fetch.

    Vulnerable vs Fixed

    Vulnerable — fetches anywhere, follows redirects
    // app/api/preview/route.ts
    export async function POST(req: Request) {
      const { url } = await req.json();
    
      // Whatever URL the user hands us, we go get it.
      const res = await fetch(url);
      const html = await res.text();
    
      return Response.json({
        title: extractTitle(html),
        image: extractOgImage(html),
      });
    }
    Fixed — resolved, allowlisted, no redirects
    // app/api/preview/route.ts
    import { lookup } from 'dns/promises';
    import ipaddr from 'ipaddr.js';
    
    async function isPublic(hostname: string) {
      const { address } = await lookup(hostname);
      const parsed = ipaddr.parse(address);
      const range = parsed.range(); // 'private', 'loopback', 'linkLocal', 'unicast'...
      return range === 'unicast';
    }
    
    export async function POST(req: Request) {
      const { url } = await req.json();
      const parsed = new URL(url);
    
      if (parsed.protocol !== 'https:') {
        return Response.json({ error: 'https only' }, { status: 400 });
      }
      if (!(await isPublic(parsed.hostname))) {
        return Response.json({ error: 'internal address' }, { status: 400 });
      }
    
      // Resolve once, forbid redirects so the check cannot be bypassed.
      const res = await fetch(parsed.toString(), {
        redirect: 'error',
        signal: AbortSignal.timeout(5_000),
      });
      const html = await res.text();
    
      return Response.json({
        title: extractTitle(html),
        image: extractOgImage(html),
      });
    }

    Three things matter. Resolve the hostname yourself before the fetch. Reject private, loopback, and link-local ranges. Disable redirect following — otherwise attacker.com can 302 into 169.254.169.254 and the check you did on the original URL is useless.

    A real case

    Capital One — 100 million records via a single SSRF

    In 2019 a misconfigured WAF let an attacker ask an EC2 instance to fetch a URL. That URL was the AWS metadata endpoint. The stolen temporary credentials opened the bucket with 100 million credit applications in it.

    Related reading

    Glossary

    Server-Side Request Forgery (SSRF)Broken Object Level Authorization (BOLA)

    What we find

    ssrf

    References

    • API7: Server Side Request Forgery — official OWASP entry
    • OWASP API Security Top 10 (2023) — full list
    • CWE-918 on cwe.mitre.org

    Find every endpoint that fetches a URL for you.

    Flowpatrol probes every webhook, image proxy, and URL importer in your app and confirms which ones follow the user inside your network. Five minutes. One URL.

    Try it free