Why is this an issue?
Server-Side Request Forgery (SSRF) occurs when attackers can coerce a server to perform arbitrary requests on their behalf.
An SSRF vulnerability can either be basic or blind, depending on whether the server’s fetched data is directly returned in the web application’s
response.
The absence of the corresponding response for the coerced request on the application is not a barrier to exploitation and thus must be
treated in the same way as basic SSRF.
What is the potential impact?
SSRF usually results in unauthorized actions or data disclosure in the vulnerable application or on a different system it can reach. Conditional to
what is reachable, remote command execution can be achieved, although it often requires chaining with further exploitations.
Information disclosure is SSRF’s core outcome. Depending on the extracted data, an attacker can perform a variety of different actions that can
range from low to critical severity.
Below are some real-world scenarios that illustrate some impacts of an attacker exploiting the vulnerability.
Local file read to host takeover
An attacker manipulates an application into performing a local request for a sensitive file, such as ~/.ssh/id_rsa
, by using the File
URI scheme file://
.
Once in possession of the SSH keys, the attacker establishes a remote connection to the system hosting the web
application.
Internal Network Reconnaissance
An attacker enumerates internal accessible ports from the affected server or others to which the server can communicate by iterating over the port
field in the URL http://127.0.0.1:{port}
.
Taking advantage of other supported URL schemas (dependent on the affected system), for
example, gopher://127.0.0.1:3306
, an attacker would be able to connect to a database service and perform queries on it.
How to fix it in Python Standard Library
Code examples
The following code is vulnerable to SSRF as it opens a URL defined by untrusted data.
Noncompliant code example
from flask import request
from urllib.request import urlopen
@app.route('/example')
def example():
url = request.args["url"]
urlopen(url).read() # Noncompliant
Compliant solution
from flask import request
from urllib.parse import urlparse
from urllib.request import urlopen
SCHEMES_ALLOWLIST = ['https']
DOMAINS_ALLOWLIST = ['trusted1.example.com', 'trusted2.example.com']
@app.route('/example')
def example():
url = request.args["url"]
if urlparse(url).hostname in DOMAINS_ALLOWLIST and urlparse(url).scheme in SCHEMES_ALLOWLIST:
urlopen(url).read()
How does this work?
Pre-Approved commands
Create a list of authorized and secure URLs that you want the application to be able to request.
If a user input does not match an entry in
this list, it should be rejected because it is considered unsafe.
Important note: The application must do validation on the server side. Not on client-side front-ends.
Pitfalls
The trap of 'StartsWith' and equivalents
When validating untrusted URLs by checking if they start with a trusted scheme and authority pair scheme://authority
, ensure
that the validation string contains a path separator /
as the last character.
If the validation string does not contain a terminating path separator, the SSRF vulnerability remains; only the exploitation technique
changes.
Thus, a validation like startsWith("https://example.com")
or an equivalent with the regex ^https://example\.com.*
can be
exploited with the following URL https://example.commit.malicious.io
.
How to fix it in Requests
Code examples
The following code is vulnerable to SSRF as it performs an HTTP request to a URL defined by untrusted data.
Noncompliant code example
from flask import request
import requests
@app.route('/example')
def example():
url = request.args["url"]
requests.get(url).content # Noncompliant
Compliant solution
from flask import request
import requests
from urllib.parse import urlparse
DOMAINS_ALLOWLIST = ['trusted1.example.com', 'trusted2.example.com']
@app.route('/example')
def example():
url = request.args["url"]
if urlparse(url).hostname in DOMAINS_ALLOWLIST:
requests.get(url).content
How does this work?
Pre-Approved commands
Create a list of authorized and secure URLs that you want the application to be able to request.
If a user input does not match an entry in
this list, it should be rejected because it is considered unsafe.
Important note: The application must do validation on the server side. Not on client-side front-ends.
The compliant code example uses such an approach. The requests
library implicitly validates the scheme as it only allows
http
and https
by default.
Pitfalls
The trap of 'StartsWith' and equivalents
When validating untrusted URLs by checking if they start with a trusted scheme and authority pair scheme://authority
, ensure
that the validation string contains a path separator /
as the last character.
If the validation string does not contain a terminating path separator, the SSRF vulnerability remains; only the exploitation technique
changes.
Thus, a validation like startsWith("https://example.com")
or an equivalent with the regex ^https://example\.com.*
can be
exploited with the following URL https://example.commit.malicious.io
.
Resources
Standards