Security vulnerabilities such as CSRF (Cross-site request forgery) are well known by most web developers and Symfony provides automatic protection against them. A related but lesser known vulnerability is called SSRF (Server-side request forgery).
SSRF allows an attacker to induce the backend application to make HTTP requests to an arbitrary domain. These attacks can also target the internal hosts and IPs of the attacked server. The following simplified example has been extracted from this article, which explains the problem in detail:
Step 1: Your backend admin is freely accessible but only from internal IPs
(e.g. https://192.168.0.68/admin
).
Step 2: Your web application makes API requests like the following to get certain information (e.g. the stock of a product):
1 2 3 4 5
POST /product/stock HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 118
stockApi=https://stock.weliketoshop.net:8080/product/stock/check%3FproductId%3D6%26storeId%3D1
Step 3: The attacker can submit the following request to access to your backend admin:
1 2 3 4 5
POST /product/stock HTTP/1.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 118
stockApi=https://192.168.0.68/admin
The solution, as it happens with many security vulnerabilities, requires
filtering the user input (in this case, the IP address requested by the user).
In Symfony 5.1, we improved the HttpClient component to add a new
NoPrivateNetworkHttpClient
that blocks all internal IP addresses by default.
This new client decorates the default HttpClient, so you can use it as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\NoPrivateNetworkHttpClient;
$client = new NoPrivateNetworkHttpClient(HttpClient::create());
// nothing changes when requesting public networks
$client->request('GET', 'https://example.com/');
// however, all requests to private networks are now blocked by default
$client->request('GET', 'http://localhost/');
// the second optional argument defines the networks to block
// in this example, requests from 104.26.14.0 to 104.26.15.255 will result in an exception
// but all the other requests, including other internal networks, will be allowed
$client = new NoPrivateNetworkHttpClient(HttpClient::create(), ['104.26.14.0/23']);
The application is already broken at this point, isn't it? You should never let the user input the URL which is used to communicate with. And even if you do, you should validate it.
This is equal to mysql_query("SELECT * FROM table WHERE id = {$_POST['id']}"); :-P
@naitsirch it's a bit more complex, e.g. think about redirections: the initial URL can look OK, but then it could redirect to some internal IP. Think also about a crawler that follows links found in some HTML pages / hypermedia API.
Input validation also doesn't work with lying DNS servers: imagine if validation decides it's safe because the DNS replies with a non-internal IP, but when the HTTP client does the DSN resolution the DNS replies with an internal IP? Such DNS servers exist into the wild. NoPrivateNetworkHttpClient also protects against this.
Beaten to fr1st post by naitsirch :-) I had to first read fully the linked post to understand what was the specific attack scenario that we are defending against.
And I fully agree: if your API lets the user specify hostnames for your webserver to make http calls to, you f**d up real bad, and should probably first learn a bit about security, then review the whole app for other obvious, glaring mistakes and fix them.
In fact, I suspect that using the NoPrivateNetworkHttpClient will provide a false sense of security to novice developers, and hence make the situation worse :-(
What am I talking about? Simple: if the end user can decide what the target hostname is, then blacklisting private-network-ips is not enough, as it means that the server is still an open-relay that allows anyone to use it to make requests to the internet at large and obfuscate the source IP address. Helpful in covering up your tracks as well in setting up DOS attacks...
At the very least, as whitelist approach should be preferred to the blacklist approach.
@ngrekas agree that this can be useful in other scenarios than the described, where a whitelist approach can not be used (eg. an app where the editorial team can add urls of 3rd party rss feeds to source contents from), as well as to prevent poisoning attacks.
ps: the described scenario is in fasct equivalent to
"SELECT * FROM {$_POST['table_id']}"