Nette Http 3.4.0 Brings Protection Against SSRF Attacks

4 hours ago by David Grudl  

A user avatar. An image generated by an MCP server. An OG preview in a comment. A webhook callback. An OAuth redirect. An RSS feed import. Whenever your application fetches a URL submitted by a user (or an AI assistant), it becomes a target for Server-Side Request Forgery (SSRF), one of the OWASP Top 10 vulnerabilities.

Nette/Http 3.4.0 introduces two new classes, UrlValidator and IPAddress, that solve this problem in a single line.

How SSRF Attacks Work in Cloud Environments

A classic SSRF attack looks innocent:

https://169.254.169.254/latest/meta-data/iam/security-credentials/

The address 169.254.169.254 is a cloud metadata endpoint, reachable only from inside an AWS, Azure or GCP instance. An attacker can't reach it from the outside, but your server can. If the application blindly fetches that URL (say, as a “user profile picture”), the attacker gets back active IAM credentials.

Other popular targets: https://192.168.1.1/admin/ (an internal router), http://localhost:6379/ (Redis with no auth), https://10.0.5.7/internal-api/ (anything on your LAN).

A naive “is it https://?” check isn't enough. You need to know which IP addresses the URL actually points to, not how it looks. That means resolving the name to IP addresses via DNS, checking them against a list of forbidden ranges and handling gotchas like IPv4-mapped IPv6 or DNS rebinding… it quickly turns into fifty lines of code that every project solves a little differently, usually wrong.

With the rise of MCP servers and AI integrations, the attack surface has grown dramatically. The AI receives a URL from the user and passes it to a tool to fetch. The server downloads the URL. The AI has no intuition for “this is an internal endpoint”, so without this protection an AI agent makes an ideal target for an attack.

How to Validate URLs in PHP

use Nette\Http\UrlValidator;

if (!(new UrlValidator)->allows($userUrl)) {
	return; // unsafe URL
}
// now it's safe to fetch

The default is strict: https only, port 443 only, the hostname must resolve exclusively to public IP addresses. It blocks loopback, private ranges, link-local including cloud metadata, multicast and IANA-reserved.

When you need different rules, you configure them in the constructor:

// Internal monitoring needs the LAN (http and any port)
new UrlValidator(schemes: ['http', 'https'], ports: null, allowPrivateIps: true);

// OAuth only to known partners
new UrlValidator(
	hostAllowlist: ['*.partner1.com', '*.partner2.com'],
);

The wildcard *.example.com matches subdomains of any depth. Add the apex domain separately.

Preventing DNS Rebinding Attacks in PHP

Between allows() and the actual fetch there's a short window where an attacker who controls the host's DNS switches the A record from a public to an internal IP. Validation passes, but the request goes elsewhere (a DNS rebinding attack).

For full defense there's getResolvedIPs(). It works just like allows(): for an unsafe URL it returns [], for a safe one an array of validated addresses. You then pass those to cURL via CURLOPT_RESOLVE:

$ips = (new UrlValidator)->getResolvedIPs($url);
if (!$ips) {
	return; // unsafe
}

$ch = curl_init($url);
$host = parse_url($url, PHP_URL_HOST);
curl_setopt($ch, CURLOPT_RESOLVE, ["$host:443:" . implode(',', $ips)]);

cURL no longer makes a new DNS query, the connection goes exactly to the IPs that passed validation. The window for DNS rebinding closes.

Validating IPv4 and IPv6 Addresses in PHP

Alongside comes IPAddress, an immutable object for working with IPv4 and IPv6 addresses:

use Nette\Http\IPAddress;

$ip = new IPAddress('169.254.169.254');
$ip->isLinkLocal();                // true (cloud metadata)
$ip->isInRange('192.168.0.0/16');  // false

Full support for IPv4-mapped IPv6: ::ffff:127.0.0.1 evaluates as loopback. A naive check doesn't recognize this form as loopback, so an attacker easily bypasses its protection through it.

Usable independently of UrlValidator anywhere you work with IPs: rate limiting, audit logs, trusted proxy validation. The static IPAddress::isValid() as a quick checker, IPAddress::tryFrom() as a factory returning ?self without an exception.


What this pair doesn't solve: HTTP redirects (your client should have them disabled, or revalidate every hop) and attacks targeting the content of the fetched response itself. Validating the address is not validating the content.

What else 3.4 brings

SSRF protection is the flagship, but version 3.4 brings more:

  • The End of CSRF Tokens: The new Request::isFrom() method identifies the origin of a request using Sec-Fetch-* headers, and this is such a big topic that we’ve dedicated a separate article to it.
  • More modern cookies: setCookie() now sends the Max-Age attribute, supports partitioned cookies (CHIPS) via the partitioned: true parameter and automatically enforces Secure for SameSite=None or partitioned cookies.
  • The SameSite enum: instead of the string constants IResponse::SameSiteLax and friends, you now write SameSite::Lax. Both setCookie() and Session::setCookieParameters() accept it, the old constants are deprecated.
  • Expiration the same everywhere: setCookie(), Session::setExpiration() and section expiration all interpret the value the same way: a number is a relative count of seconds, a string is an interval ('20 minutes') or a date; setCookie() additionally accepts DateTimeInterface. Passing an absolute UNIX timestamp is deprecated, and a session cookie is now requested with the value null instead of the former 0.
  • detectLanguage() understands the wildcard: the Accept-Language header may contain * (mainly sent by API clients and bots). Previously the method ignored it and returned null, now it returns one of the languages the application offers.
  • The library now requires PHP 8.3+, the deprecated Request::getRemoteHost() method returns null, and the long-deprecated UserStorage class has been removed.

Everything described above ships with nette/http 3.4.0.

David Grudl Founder of Uměligence and creator of Nette Framework, the popular PHP framework. Since 2021, he's been fully immersed in artificial intelligence, teaching practical AI applications. He discusses weekly tech developments on Tech Guys with his co-hosts and writes for phpFashion and La Trine. He believes AI isn't science fiction—it's a practical tool for improving life today.