How to avoid SSRF vulnerability in Go applications
Go undoubtedly continues to gain popularity for building scalable and efficient web applications, and as such, it becomes imperative to address security vulnerabilities like SSRF. Go's concurrency model and performance make it an excellent choice for server-side applications, but developers must remain vigilant against potential security threats.
In this article, we will learn how SSRF vulnerabilities manifest in Go applications, and how developers can implement effective security measures to protect their applications and data.
We will analyze the code step-by-step to identify the SSRF vulnerability and discuss the security risks associated with it. Additionally, we will introduce tools like Snyk Code that can help developers detect and fix SSRF vulnerabilities before deploying their applications to production.
What is an SSRF vulnerability?
Server-Side Request Forgery (SSRF) is a critical security vulnerability that allows an attacker to make requests from a server-side application to unintended destinations. This can lead to unauthorized access to internal services, sensitive data exposure, and even remote code execution. SSRF exploits the trust that servers place in internal network communications, allowing attackers to manipulate URLs and redirect requests to malicious endpoints.
Common attack vectors and impacts of SSRF
SSRF vulnerabilities often arise when an application accepts user input to construct URLs for server-side requests without proper validation or sanitization. Attackers exploit this by crafting malicious input that redirects the server's request to an unintended target, such as:
Internal services (e.g., metadata services in cloud environments)
Localhost interfaces
Other internal network resources
The impact of an SSRF vulnerability if exploited in Go applications by attackers can be severe, including:
Data exfiltration: Accessing sensitive data from internal services, such as other microservices accessible to the vulnerable Go application.
Service disruption: Sending requests that overload or disrupt internal services.
A vulnerable Go application
The following Go application is a simple HTTP server built using the Gin web framework. It exposes a POST
endpoint /cloudpawnery/image
that allows users to download and resize an image file. Here's a breakdown of the key components:
Configuration and Go structs:
Given the following Go code in our web program, we are going to define some initial configurations for the functionality of fetching files remotely and the expected data schema in the incoming requests on the API route:
const baseHost = "localtest.me:8080"
type FileInfo struct {
Filename string `json:"filename"`
Download string `json:"download"`
}
In the above code snippet:
baseHost is a constant that defines the base domain used to construct the URL for downloading files.
FileInfo is a struct that represents the JSON structure expected from the file storage server.
A function to download over HTTP and resize assets
The downloadAndResize
function takes tenantID
, fileID
, and fileSize
as parameters. It constructs a URL to download a JSON file containing information about the file to be downloaded and resized.
func downloadAndResize(tenantID, fileID, fileSize string) error {
// Example input based on the mocked storage server in fixtures/http directory of the project:
log.Printf("Processing request for tenantID: %s, fileID: %s", tenantID, fileID)
urlStr := fmt.Sprintf("http://%s.%s/storage/%s.json", tenantID, baseHost, fileID)
fmt.Println("Resolved URL: ", urlStr)
// Parse the URL to extract the hostname
parsedURL, err := url.Parse(urlStr)
if (err != nil) {
panic(err)
}
fmt.Println("Resolved Hostname: ", parsedURL.Hostname())
// Make HTTP request
resp, err := http.Get(urlStr)
if (err != nil) {
panic(err)
}
defer resp.Body.Close()
// Read response body
body, err := ioutil.ReadAll(resp.Body)
if (err != nil) {
panic(err)
}
// Decode JSON data
var info FileInfo
err = json.Unmarshal(body, &info)
if (err != nil) {
panic(err)
}
// Download file
downloadResp, err := http.Get(info.Download)
if (err != nil) {
panic(err)
}
defer downloadResp.Body.Close()
// Create target filename
targetFilename := fmt.Sprintf("uploads/%s", info.Filename)
// read the downloaded file into memory
fileBytes, err := ioutil.ReadAll(downloadResp.Body)
if (err != nil) {
panic(err)
}
// Save downloaded file
err = ioutil.WriteFile(targetFilename, fileBytes, 0644)
if (err != nil) {
panic(err)
}
convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
fmt.Println("Running command:", convertCmd)
_, err = exec.Command("sh", "-c", convertCmd).Output()
if (err != nil) {
fmt.Println("Error resizing image:", err)
} else {
fmt.Println("Downloaded and resized image:", targetFilename)
}
return nil
}
The function performs the following steps:
Constructs the URL using the
tenantID
andfileID
.Makes an HTTP GET request to fetch the JSON data.
Parses the JSON to extract the download URL and filename.
Downloads the file and saves it locally.
Resize the image using the
convert
command.
Our main Go function
In the heart of the Go program is where we instantiate a web server, define a POST endpoint route, and invoke the downloadAndResize
function:
func main() {
// Create a Gin router
router := gin.Default()
// Define a POST endpoint
router.POST("/cloudpawnery/image", func(c *gin.Context) {
// If data lives on the query string we can use this:
tenantID := c.Query("tenantID")
fileID := c.Query("fileID")
fileSize := c.Query("fileSize")
if (fileSize == "") {
fileSize = "200"
}
// Validate tenantID and fileID
if (tenantID == "" || fileID == "") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing tenantID or fileID"})
return
}
// Call the download and resize function
err := downloadAndResize(tenantID, fileID, fileSize)
if (err != nil) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Return a success response
c.JSON(http.StatusOK, gin.H{"message": "File downloaded and resized successfully"})
})
// Start the HTTP server
router.Run(":7000")
}
The POST endpoint is defined with the HTTP route /cloudpawnery/image
that extracts tenantID
, fileID
, and fileSize
from the query parameters. It also performs the following tasks:
- Validates the presence of tenantID
and fileID
.
- Calls downloadAndResize
to process the image.
- Returns a JSON response indicating success or failure.
Identifying the SSRF vulnerability in the code
The SSRF vulnerability in this Go application arises from the way the tenantID
is used to construct the URL for the HTTP request.
Let’s break it down to form a detailed analysis of the security vulnerability:
User-Controlled Input: The
tenantID
is extracted directly from the query string of the HTTP request without any validation or sanitization. This allows an attacker to manipulate thetenantID
to point to an arbitrary domain.
URL Construction: The downloadAndResize
function constructs a URL using the tenantID
and fileID
. This is a good opportunity to ask yourself if this string concatenation is a secure way to construct a URL? What could go wrong?
urlStr := fmt.Sprintf("http://%s.%s/storage/%s.json", tenantID, baseHost, fileID)
If an attacker provides a malicious tenantID
, they can control the entire subdomain, potentially redirecting the request to a malicious server.
HTTP Request: The application makes an HTTP GET request to the constructed URL:
resp, err := http.Get(urlStr)
This request can be redirected to an attacker-controlled server, allowing them to access internal services or perform other malicious actions.
The vulnerable Go code that leads to SSRF
In the provided Go application, the tenantID
is extracted from the query string of an HTTP request and directly used to construct a URL. This URL is then used in an http.Get
GET request function without any validation or sanitization.
This practice opens the door to a Server-Side Request Forgery (SSRF) vulnerability. By manipulating the tenantID
, an attacker can craft a URL that points to an unintended host, potentially accessing sensitive internal services or data.
Exploiting the SSRF vulnerability with malicious tenantID
An attacker can exploit this vulnerability by providing a malicious tenantID
redirecting the request to an unintended server.
For example, if the attacker sends a request with a tenantID
like e:@evil.com/url=
, the constructed URL becomes http://1234:e@evil.com/url=.localtest.me:8080/storage/fileID.json
. This URL would now be fetching JSON information from the evil.com
website instead of the local microservice, and the attacker would be able to control the storage file configuration.
Leveraging Snyk Code to detect SSRF and security analysis
How did I find this SSRF vulnerability lurking in my Go code, you ask?
I have the Snyk extension installed in my VS Code IDE which adds a red squiggly line when I save my code, and Snyk detects that I introduced an insecure code line that may result in a security vulnerability.
Here is how Snyk finds and alerts about Server-side Request Forgery vulnerabilities in real-time:
As you can see, Snyk doesn’t just find the security vulnerabilities but also provides a detailed code-path call flow of the input controlled by the user and makes it to the sink function in the form of an HTTP call.
Snyk goes a step further and also shows me several suggested fixes of how to mitigate the security issue from commits made in other open source projects.
To get the same experience and secure your code too, install the Snyk extension or simply connect your GitHub, GitLab, or Bitbucket code repository to the Snyk app and it will monitor it continuously to provide these detections and fixes.
As a follow-up security education, I’d like to recommend Go developers to read up on Go security best practices.
Bridge the gap between security and development
Discover the six pillars for DevSecOps success and how they can apply to your organization.