Server Side Request Forgery ('SSRF')

ID

go.server_side_request_forgery

Severity

critical

Remediation Complexity

medium

Remediation Risk

medium

Remediation Effort

medium

Resource

Channel

Language

Go

Tags

ASVS50:v1.3.6, ASVS50:v13.2.5, CWE:918, NIST.SP.800-53, OWASP:2021:A10, PCI-DSS:6.5.8

Description

Improper validation of external input used to retrieve the content of a URL ('SSRF').

SSRF vulnerabilities arise when an application accepts user input to create or control URLs or networking requests without appropriate validation or restriction. This can allow attackers to craft malicious requests to sensitive internal endpoints or external services on behalf of the server.

Rationale

Similar to other injection attacks, an SSRF vulnerability allows an attacker to manipulate the URLs used by a server-side application, to point to unexpected internal or external resources.

By carefully selecting the URLs, the attacker can read or update internal resources, such as cloud metadata, internal services, or databases. In addition, SSRF can bypass network access controls like firewalls and VPNs.

An SSRF vulnerability can be exploited by threat actors for network reconnaissance, data exfiltration, denial of service or pivot attacks to other systems.

For instance, consider a Golang application that fetches resources based on user-provided URLs:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
)

func handler(w http.ResponseWriter, req *http.Request) {
	// Vulnerable: user input directly used in HTTP request
	url := req.URL.Query().Get("url")
	resp, err := http.Get(url)
	if err != nil {
		http.Error(w, "Failed to fetch URL", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, "Error reading response", http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Response: %s", body)
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

In this example, the application uses user-controlled input to make an HTTP request, allowing an attacker to target internal services by providing a crafted URL.

Severity when the destination server is fixed

The severity in the table above applies when the external input can reach the destination of the request. When the URL is built from a constant prefix that already fixes the scheme, the host and the port, the input lands after the authority and can only choose a path or a query string, so the request cannot be steered to another server. The detector recognizes this and reports the finding at low severity instead.

With BASE a constant "https://api.internal.example.com", a URL built as BASE + "/" + target puts target in the path: it selects which endpoint of that fixed server is invoked, not which server is invoked.

The finding is lowered rather than dropped, because the input still decides which endpoint of that server runs and may reach operations that were not meant to be callable this way. A path is also not always inert: if the fixed host answers with a redirect, an HTTP client that follows redirects can still end up requesting a location the caller did not choose.

The severity is left unchanged whenever the input can still affect the destination:

  • it reaches the scheme, the host or the port - for example SCHEME + target + "/api", "https://" + tenant + ".example.com/api" or BASE + ":" + port + "/api";

  • the prefix does not close the authority, as in BASE + suffix, where the input can extend the host name into a different domain;

  • the prefix cannot be resolved to a constant - a value imported from another module, a parameter default, or a name that is assigned again after its declaration, is not treated as fixed;

  • the prefix does not describe a host that a URL could carry, one with an empty label say. That is not a URL anyone writes, it is what is left when part of the expression could not be resolved, and the part that could not be resolved is the one under external control.

It is also left unchanged when the fixed host is itself the sensitive target, because there choosing the path is the attack rather than a limitation of it: with a URL fixed to http://169.254.169.254/latest, the path selects which instance credentials are read. This covers cloud instance-metadata endpoints, loopback addresses, Kubernetes and cloud-internal service names, and private or otherwise reserved address ranges. The check is purely lexical, so a public host name that resolves to a private address at run time is not recognized.

Remediation

To mitigate SSRF, validate and restrict the URLs that the application can access. Use allowlists to define acceptable domains and ports, and avoid accessing sensitive internal services based on external input.

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

func handler(w http.ResponseWriter, req *http.Request) {
	userURL := req.URL.Query().Get("url")

	// Validate and restrict URL
	if !isValidURL(userURL) {
		http.Error(w, "Invalid URL", http.StatusBadRequest)
		return
	}

	resp, err := http.Get(userURL)
	if err != nil {
		http.Error(w, "Failed to fetch URL", http.StatusInternalServerError)
		return
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, "Error reading response", http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "Response: %s", body)
}

// Simple validation function using a domain allowlist
func isValidURL(rawURL string) bool {
	parsedURL, err := url.Parse(rawURL)
	if err != nil || !strings.HasPrefix(parsedURL.Scheme, "http") {
		return false
	}

	allowedDomains := []string{"example.com", "api.example.com"}

	for _, domain := range allowedDomains {
		if strings.HasSuffix(parsedURL.Hostname(), domain) {
			return true
		}
	}
	return false
}

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

In this secure example, the function isValidURL checks if the URL belongs to an allowed set of domains, ensuring that only appropriate and safe requests are sent, protecting against SSRF attacks.

Configuration

The detector has the following configurable parameters:

  • sources, that indicates the source kinds to check.

  • neutralizations, that indicates the neutralization kinds to check.

Unless you need to change the default behavior, you typically do not need to configure this detector.

References

© 2026 Xygeni. All rights reserved