NewApp Monitoring is liveSign up free
AgentbeamBeta
Course outline

Learn / Level 3 / lesson 6

Server-side request forgery

How a feature that fetches a URL for you can be steered to internal addresses you could not reach yourself, and how to defend it with allowlists and address checks. Includes the Plex Media Server CVE.

8 min read · Yash Thakker · updated 2026-10-24

SSRF · CWE-918 · cloud security · secure coding

An outsider asking a public server to fetch an address inside a private network

What you will learn

  • Explain why a server can reach addresses an outsider cannot
  • Recognise features that fetch user-supplied URLs
  • Defend a fetcher with allowlists, address checks and no redirects
  • Describe the Plex Media Server SSRF record

Before this: OS command injection, How the internet moves data

Many features need a server to fetch something from the internet for you: a link preview, an image import, a webhook, a PDF generated from a URL, a media transcode of a remote file. Whenever the address comes from outside, the server becomes a tool the outsider can aim. This is server-side request forgery, or SSRF.

Why the server's position matters

Recall how the internet moves data: addresses reach machines, and networks are often split into public and private parts. Private ranges (such as those beginning 10., 192.168. and 127., the last meaning "this very machine") are not reachable from the internet. A company's admin panels, databases, build servers and monitoring pages often live there, protected mainly by the fact that outsiders cannot reach them.

Your web server, though, sits inside that network. It can talk to those private addresses freely. If an outsider can make the server send requests to a destination of their choosing, then the outsider can reach private things through the server, with the server's network position and sometimes its credentials.

The vulnerable pattern

Consider a link-preview feature:

import urllib.request

def preview(url):
    with urllib.request.urlopen(url, timeout=5) as r:
        return r.read(2000)

Step by step: an outsider submits an internal address, the server fetches it from inside the network, and the private answer is returned to the outsider

For https://example.com/article, this works. For an address like http://127.0.0.1:8080/admin or a private address on the company network, the server obediently fetches it, because from the server's position it is perfectly reachable, and returns the content. The visitor never touched the internal service; the server did on their behalf. This is , weakness class CWE-918.

The impact depends on what is reachable and what is returned. Typical outcomes:

  • Reading internal pages and APIs that have no login because "only we can reach them".
  • Scanning which internal addresses and ports respond, using the server as a probe.
  • On cloud machines, reaching the , a local address that describes the machine and, in some configurations, supplies temporary credentials.
  • Triggering actions through internal services that treat any request from inside as trusted.

Defending a fetcher

No single trick closes this. Layer these, starting from the strongest:

1. Allowlist destinations. If the feature only ever imports from a handful of known hosts, accept only those. That is the best fix, and it also removes most of the surprises below.

2. If any address is allowed, check the address, not the name. Resolve the hostname to its IP address, refuse private, loopback, link-local and other internal ranges, and then connect to that same address. Checking only the text of the URL fails, because an attacker can control a public name that points at an internal address, and numeric addresses can be written in several forms.

import ipaddress, socket
from urllib.parse import urlparse

def is_public(host):
    ip = ipaddress.ip_address(socket.gethostbyname(host))
    return ip.is_global and not ip.is_multicast

def check(url):
    p = urlparse(url)
    if p.scheme not in ("http", "https") or not p.hostname:
        raise ValueError("only http and https")
    if not is_public(p.hostname):
        raise PermissionError("internal address")
    return url

3. Do not follow redirects blindly. A public address can answer with a redirect to an internal one. Disable redirects or re-run the check for each hop.

4. Limit schemes and size. Allow only http and https (not file: or others), cap response size and time, and do not return the raw response if you can return only what you need.

5. Reduce what the server can reach. Egress filtering means restricting outbound traffic from the server to what it actually needs. Put the fetcher in its own, isolated network segment with no route to internal services, and require protections on cloud metadata (providers offer a hardened version that rejects simple forwarded requests).

6. Authenticate internal services. "Only reachable from inside" is not authentication. If every internal service requires its own credentials, a forged request gets nothing.

The guarded flow: resolve the hostname, check the address is public, then connect to that same address, refusing internal ones

A caution about the code above: name resolution can change between your check and the connection. Stronger designs resolve once and connect directly to the checked IP address, or use a dedicated fetching proxy that enforces the rules. Do not claim a hand-written check is complete.

Why is it not enough to reject URLs that contain the text '127.0.0.1'?

The same address has other spellings, a hostname can resolve to it, and a redirect can lead there. Checks must be made on the resolved address, ideally with a destination allowlist as well.

Spotting it in a code review

  • Look for features that take a URL, hostname or address and fetch it: previews, importers, webhooks, avatars from URL, document and image renderers, proxy endpoints.
  • Look at the HTTP client call and ask whether the destination is allowlisted or address-checked.
  • Check redirect handling and which URL schemes are accepted.
  • Ask what network the server sits in, and what is reachable from it. The same code is a bigger problem on a flat internal network.
  • Ask whether cloud metadata protections are on.

A real case

Two details teach more than the headline. First, the base severity score in the record is moderate (4.3 on the v3.1 scale and 5.3 on v4), yet it appears on the exploited list. A low-looking score does not mean low interest: SSRF is valued because of what lies behind it, which a generic score cannot see. Second, it required a valid login, but media servers are commonly reachable from the internet with many ordinary user accounts. For reference, the fix is to update to a version at or after the one named in the record.

How AI changes this

Attackers. SSRF is context-heavy: whether it matters depends on what the server can reach. Models can read a codebase to find fetch calls, infer the surrounding cloud setup, and propose destinations to try, which shortens the time from finding a bug to judging its worth. Automated scanners can also test many products' URL parameters quickly.

Defenders. An assistant can list every outbound HTTP call in a repository and group them by whether the destination is fixed or input-derived. That inventory is the starting point for the allowlist. Review its answers, because it can miss calls made through helper libraries.

AI agents. Agents that browse or fetch URLs are natural SSRF targets, and they are special because they decide for themselves what to fetch, based on text they read. A page can tell an agent "go fetch this internal address" and the agent, running inside your network or on your laptop, may be able to. Combined with a private network position, that can expose internal tools or credentials. Defences match the lesson: allowlist what an agent may fetch, use egress filtering on its environment, and keep credentials out of reach of the process that browses. Beam's AI agent web and transport security guide puts "places where a client fetches an address supplied by a server" high in its review order, and the dictionary entry on explains the control. Agents using tools over MCP add a second path, since a tool server can return URLs the client then fetches.

Try it

This lab starts a tiny "internal service" on your own machine and shows a naive fetcher reaching it. Everything uses the local loopback address, so nothing leaves your computer.

Save as ssrf_lab.py:

import threading, ipaddress, socket, urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse

class Internal(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200); self.end_headers()
        self.wfile.write(b"internal-only page: pretend admin data")
    def log_message(self, *a): pass

server = HTTPServer(("127.0.0.1", 8765), Internal)
threading.Thread(target=server.serve_forever, daemon=True).start()

def naive(url):
    return urllib.request.urlopen(url, timeout=3).read().decode()

def guarded(url):
    host = urlparse(url).hostname
    ip = ipaddress.ip_address(socket.gethostbyname(host))
    if not ip.is_global:
        raise PermissionError("refused: " + str(ip) + " is not a public address")
    return urllib.request.urlopen(url, timeout=3).read().decode()

target = "http://127.0.0.1:8765/admin"
print("naive:  ", naive(target))
try:
    print("guarded:", guarded(target))
except PermissionError as e:
    print("guarded:", e)
server.shutdown()

Run python3 ssrf_lab.py. The naive fetcher returns the internal page; the guarded one refuses. Read the guarded function again and list what it still does not handle (redirects, name changes between check and connect). Delete the file afterwards.

Check yourself

Why can a server reach things that the person who sent the request cannot?

The server sits inside the private network and is trusted by internal services, while the outsider is on the public internet. A forged request borrows the server's position.

Name two defences, and say which is strongest.

Allowlisting permitted destinations is strongest. Others: resolve and check the address is public, do not follow redirects blindly, restrict schemes, filter outbound traffic from the server, require metadata protections, and authenticate internal services.

The Plex record scores 4.3 to 5.3 yet is on the exploited list. What does that tell you about scores?

A base score describes the flaw in general, not what is reachable from your server. SSRF value depends on the internal network behind it, so a moderate score can still be actively attacked, and exploitation evidence like the KEV list matters when prioritising.

Next

Next we look at bytes a program receives and turns back into live objects, and why that can be the same as running someone else's instructions: unsafe deserialization.