Server-side request forgery (SSRF) - Go

Server-side request forgery (SSRF) - Go

Need

Mitigation of server-side request forgery (SSRF) attacks

Context

  • Usage of Go 1.16 for developing high-performance and efficient applications
  • Usage of gin-gonic/gin for building web applications in Go
  • Usage of net/http for building HTTP servers in a Node.js application
  • Usage of io/ioutil for reading and writing files in Node.js

Description

Non compliant code

        package main

import (
  "github.com/gin-gonic/gin"
  "net/http"
  "io/ioutil"
)

func main() {
  router := gin.Default()
  router.GET("/proxy", func(c *gin.Context) {
    target := c.Query("target")
    resp, err := http.Get(target)
    if err != nil {
      c.JSON(http.StatusInternalServerError, gin.H{"error": "Error when making the request"})
      return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
      c.JSON(http.StatusInternalServerError, gin.H{"error": "Error when reading the response"})
      return
    }
    c.String(http.StatusOK, string(body))
  })
  router.Run()
}
        
        

This is a simple Go server application using the Gin framework. It has a "/proxy" endpoint that takes a "target" URL as a query parameter and sends a GET request to that URL.

The vulnerability lies in the /proxy route. In this route, the application takes a user-supplied URL (from the target query parameter) and makes a GET request to this URL using the http.Get(target) function. This is a classic example of a Server-Side Request Forgery (SSRF) vulnerability because the application server can be tricked into making arbitrary requests to any domain.

An attacker can exploit this vulnerability by sending a request to the /proxy endpoint with a target parameter pointing to an internal resource (e.g., http://localhost/admin) or to an external resource under their control. The server will then send a GET request to this URL, potentially exposing sensitive information or interacting with other services in a way that was not intended by the application developers.

Steps

  • Validate the 'target' parameter to ensure it is a valid URL.
  • Implement a whitelist of allowed domains that the application can make requests to.
  • Use a DNS resolution library to resolve the domain from the 'target' parameter and compare it against the whitelist.
  • If the domain is not in the whitelist, reject the request and return an appropriate error message.
  • Consider implementing rate limiting or request throttling to prevent abuse of the proxy endpoint.
  • Implement proper error handling and logging to track any potential SSRF attempts.

Compliant code

        package main

import (
  "github.com/gin-gonic/gin"
  "net/http"
  "io/ioutil"
  "net/url"
  "strings"
  "errors"
)

var allowedDomains = []string{
  "example.com",
  "example.org",
}

func main() {
  router := gin.Default()
  router.GET("/proxy", func(c *gin.Context) {
    target := c.Query("target")
    err := validateURL(target)
    if err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid target URL"})
      return
    }
    resp, err := http.Get(target)
    if err != nil {
      c.JSON(http.StatusInternalServerError, gin.H{"error": "Error when making the request"})
      return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
      c.JSON(http.StatusInternalServerError, gin.H{"error": "Error when reading the response"})
      return
    }
    c.String(http.StatusOK, string(body))
  })
  router.Run()
}

func validateURL(target string) error {
  u, err := url.Parse(target)
  if err != nil {
    return err
  }
  for _, domain := range allowedDomains {
    if strings.HasSuffix(u.Hostname(), domain) {
      return nil
    }
  }
  return errors.New("Invalid domain")
}
        
        

The updated code includes a function validateURL that validates the 'target' parameter to ensure it is a valid URL and that the domain is in the whitelist of allowed domains.

The allowedDomains variable is a slice of strings that contains the list of allowed domains. This list can be modified to include the domains that the application should be allowed to make requests to.

The validateURL function parses the 'target' parameter using the url.Parse function from the net/url package. If the parsing is successful, it checks if the domain (obtained using u.Hostname()) is in the allowedDomains list. If the domain is in the list, the function returns nil indicating that the URL is valid. If the domain is not in the list, the function returns an error.

In the main function, before making the HTTP request, the 'target' parameter is validated using the validateURL function. If the function returns an error, a http.StatusBadRequest is returned with an error message "Invalid target URL". If the function does not return an error, the HTTP request is made as before.

This updated code helps to mitigate the Server-side Request Forgery (SSRF) vulnerability by ensuring that the application can only make requests to a whitelist of allowed domains.

References