Nette Http 3.4.0 Brings Protection Against SSRF Attacks
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 usingSec-Fetch-*headers, and this is such a big topic that we’ve dedicated a separate article to it. - More modern cookies:
setCookie()now sends theMax-Ageattribute, supports partitioned cookies (CHIPS) via thepartitioned: trueparameter and automatically enforcesSecureforSameSite=Noneor partitioned cookies. - The
SameSiteenum: instead of the string constantsIResponse::SameSiteLaxand friends, you now writeSameSite::Lax. BothsetCookie()andSession::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 acceptsDateTimeInterface. Passing an absolute UNIX timestamp is deprecated, and a session cookie is now requested with the valuenullinstead of the former0. detectLanguage()understands the wildcard: theAccept-Languageheader may contain*(mainly sent by API clients and bots). Previously the method ignored it and returnednull, now it returns one of the languages the application offers.- The library now requires PHP 8.3+, the deprecated
Request::getRemoteHost()method returnsnull, and the long-deprecatedUserStorageclass has been removed.
Everything described above ships with nette/http 3.4.0.
Further reading
- A Quarter Century of CSRF. Now the Browser Finally Handles It
- Nette Tester: HTTP testing has never been so easy
- Nette Http 3.2: change access to credentials
- Nette Utils 4.0: UTF-8, Finder and named arguments
- Write Safer Code with the New Nette Database Documentation
- Latte 3.1: When a templating system truly understands HTML
Sign in to submit a comment