# Preventing Server-Side Request Forgery (SSRF) in PHP

> Prevent SSRF in PHP: allowlists, HTTPS only, no redirects, checking the resolved IP and limiting outgoing traffic. With cURL code.

Source: https://phpsos.de/en/blog/ssrf-in-php/  
As of: 25/10/2026  
Provider: PHP SOS, PHP emergency help and server security. Immediate help via WhatsApp: https://wa.me/491736494914?text=Hello%20PHP%20SOS%2C%20we%20need%20help%20with%20our%20PHP%20server.%20Company%3A%20%E2%80%A6%20Website%3A%20%E2%80%A6

**In short:** You prevent SSRF in PHP by having your application fetch only addresses on a fixed allowlist, exclusively over HTTPS, without following redirects. The resolved IP address is checked against private and reserved ranges and then pinned for the connection. A firewall for outgoing traffic ensures that internal systems remain unreachable even if there is a mistake in the code.

## When Your Own Server Makes Requests for Outsiders

Link previews in a chat, importing a product image via URL, webhooks to partner systems, a feed reader in the CMS: many PHP applications fetch addresses that a user has entered. This is a useful feature and at the same time a door to the inside.

With server-side request forgery, SSRF for short, an attacker gets the application to send a request to a destination of their choice. The catch: the request comes from the server itself. It thus reaches systems that are blocked from outside, such as databases, administration interfaces on the internal network, services that listen only on localhost, or the metadata services of cloud providers.

In the OWASP Top 10 of 2021, SSRF was a category of its own. In PHP projects, the vulnerability often appears where someone quickly built an "add image by URL" feature.

## SSRF in PHP: Which Functions Fetch Addresses

cURL is not the only thing that fetches addresses. You should keep an eye on the following spots:

| Function or area | Why relevant |
|---|---|
| cURL (`curl_exec()`) | most common route, many protocols possible |
| `file_get_contents()`, `fopen()`, `copy()` with a URL | use URL wrappers if `allow_url_fopen` is active |
| `getimagesize()` and image libraries | can load remote files |
| XML processing, SOAP clients | may load external resources |
| HTTP libraries such as Guzzle | build on cURL or streams, same rules |

If your application does not need to open remote files through file functions, switch off `allow_url_fopen` on the server. A related topic, including files via paths from the request, is covered by the guide [Preventing Local and Remote File Inclusion](https://phpsos.de/en/ratgeber/lfi-rfi/).

## The Defense in Five Steps

OWASP recommends a combination of measures that could each be bypassed individually but hold up together:

1. **Allowlist instead of free URL:** Ideally, the user only chooses from known destinations, or merely provides an identifier and the application builds the address itself.
2. **Allow HTTPS only:** Exclude other protocols such as `file`, `gopher` or `ftp` in cURL, including for redirects.
3. **Do not follow redirects:** Otherwise an allowed address can redirect to an internal destination.
4. **Check and pin the resolved address:** Resolve the hostname, check the IP against private, loopback and link-local ranges, and use exactly this IP for the connection. Otherwise a second DNS resolution may return a different destination.
5. **Limit the network:** The web server may reach outward only the destinations it needs.

## A Secure Pattern with cURL

The following example implements steps 1 to 4 for IPv4. Hostnames and flow are examples; the same checks apply for IPv6.

```php
<?php
declare(strict_types=1);

function ladeVonPartner(string $url): string
{
    $teile = parse_url($url);
    if (!is_array($teile) || ($teile['scheme'] ?? '') !== 'https'
        || isset($teile['user']) || isset($teile['pass']) || isset($teile['port'])) {
        throw new InvalidArgumentException('Adresse nicht erlaubt.');
    }

    $host = strtolower($teile['host'] ?? '');
    $erlaubt = ['api.partner.example', 'bilder.partner.example'];
    if (!in_array($host, $erlaubt, true)) {
        throw new InvalidArgumentException('Ziel nicht erlaubt.');
    }

    $ip = gethostbyname($host);
    $oeffentlich = filter_var($ip, FILTER_VALIDATE_IP,
        FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
    if ($oeffentlich === false) {
        throw new RuntimeException('Ziel löst auf eine nicht erlaubte Adresse auf.');
    }

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER  => true,
        CURLOPT_FOLLOWLOCATION  => false,
        CURLOPT_PROTOCOLS       => CURLPROTO_HTTPS,
        CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
        CURLOPT_RESOLVE         => [$host . ':443:' . $ip],
        CURLOPT_CONNECTTIMEOUT  => 3,
        CURLOPT_TIMEOUT         => 10,
    ]);
    $antwort = curl_exec($ch);
    if ($antwort === false) {
        throw new RuntimeException('Abruf fehlgeschlagen.');
    }
    return $antwort;
}
```

A few details are important. On failure, `gethostbyname()` returns the hostname; the subsequent IP check catches that. According to the PHP manual, the flags `FILTER_FLAG_NO_PRIV_RANGE` and `FILTER_FLAG_NO_RES_RANGE` exclude, among others, the ranges 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8 and 169.254.0.0/16. Since PHP 8.2 there is also `FILTER_FLAG_GLOBAL_RANGE`, which allows only globally valid addresses. `CURLOPT_RESOLVE` ensures that cURL uses exactly the checked address while the hostname and certificate check are retained.

Two further points limit what an attacker learns from an SSRF vulnerability. Do not return responses and error messages from the fetched target unfiltered to the user: even the distinction between "connection refused" and "timeout" reveals which internal services exist. And limit the runtime and size of the response so that the function cannot be used to overload your own server. Webhooks to addresses that customers enter themselves are best sent from a queue and not during the running request.

> **Important:**
> If your application really has to fetch arbitrary addresses, for example for link previews, move this function to a separate service without access to the internal network. An allowlist is then not possible, which makes network separation all the more important.

## The Network Layer as a Safety Net

Code checks can have gaps, for example when two libraries split a URL differently. OWASP advises treating such inconsistencies as a rejection and additionally limiting at the network level. A web server usually needs only a few destinations outward: package sources, payment and shipping providers, mail relay, certain interfaces.

A firewall for outgoing traffic helps twice. It stops SSRF requests into the internal network, and after a break-in, for example via an unpatched PHP version, it makes it harder to download further tools. How to build the rules is described in [Firewall for Web Servers: Open Only Necessary Ports](https://phpsos.de/en/blog/firewall-fuer-webserver/). Which connections a server actually establishes is shown in [Checking a Server's Outgoing Connections](https://phpsos.de/en/blog/ausgehende-verbindungen-pruefen/).

In cloud environments, the hardened variant of the metadata service is part of this. For AWS, OWASP names switching to IMDSv2 and disabling IMDSv1.

## Tracking Down SSRF in Existing Code

A quick start is to search for places where fetch functions receive values from the request:

```bash
grep -rnE "(curl_init|file_get_contents|fopen|getimagesize|copy)\s*\(" src/ --include="*.php" | grep -E '\$_(GET|POST|REQUEST)|\$url'
grep -rn "CURLOPT_FOLLOWLOCATION" src/ --include="*.php"
```

Matches are review tasks. You should look particularly thoroughly at functions that admins use: imports, webhook settings, plugin update checks.

PDF generators are easily overlooked; they convert HTML with embedded images or stylesheets into documents. If part of this HTML comes from user input, for example invoices with a freely selectable logo, the generator may fetch addresses that the user determines. Check the settings for loading remote resources there and switch it off if it is not needed.

## How does PHP SOS help?

- We find all places where your application fetches addresses, including in plugins and libraries.
- We build a central fetch function with an allowlist, protocol restriction and IP check.
- We define rules for outgoing traffic and protect metadata services in cloud environments.
- We verify the implementation from outside and document the permitted destinations.

An independent review of your application is described by [Security Audit and Penetration Test](https://phpsos.de/en/leistungen/sicherheitsaudit-penetrationstest/).


## Frequently asked questions

**Which PHP functions can be abused for SSRF?**  
All that fetch an address taken from user input: cURL, file_get_contents and fopen with URL wrappers, image and XML functions that load remote files, and libraries for webhooks, feeds or link previews.

**Is it enough to block internal IP addresses?**  
Not on its own. OWASP calls blocklists prone to bypass and recommends allowlists. In addition, the application must use exactly the address it has checked and must not follow redirects.

**Why is SSRF especially critical in the cloud?**  
Cloud providers make internal metadata services available to instances, through which credentials can sometimes be retrieved. If an application can send requests there, an attacker can reach this data through it. The providers offer hardened variants of these services, which should be enabled.

**What does a firewall for outgoing traffic achieve?**  
It limits which destinations the server can reach at all. Even if a check in the code fails, internal systems then remain unreachable. It also makes it harder, after a break-in, to download malicious code and to exfiltrate data.

## Sources

- [OWASP Server-Side Request Forgery Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)
- [PHP manual: filter constants](https://www.php.net/manual/de/filter.constants.php)
- [PHP manual: cURL constants](https://www.php.net/manual/de/curl.constants.php)
