Skip to main content

How to avoid SSRF vulnerability in Go applications

著者:
0 分で読めます

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 and fileID.

  • 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:

  1. 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 the tenantID 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.

  1. 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:

How_to_avoid_SSRF_vulns_in_go

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.