go.lang.security.injection.tainted-url-host.tainted-url-host

profile photo of semgrepsemgrep
Author
unknown
Download Count*

A request was found to be crafted from user-input $REQUEST. This can lead to Server-Side Request Forgery (SSRF) vulnerabilities, potentially exposing sensitive data. It is recommend where possible to not allow user-input to craft the base request, but to be treated as part of the path or query parameter. When user-input is necessary to craft the request, it is recommended to follow OWASP best practices to prevent abuse, including using an allowlist.

Run Locally

Run in CI

Defintion

rules:
  - id: tainted-url-host
    languages:
      - go
    message: A request was found to be crafted from user-input `$REQUEST`. This can
      lead to Server-Side Request Forgery (SSRF) vulnerabilities, potentially
      exposing sensitive data. It is recommend where possible to not allow
      user-input to craft the base request, but to be treated as part of the
      path or query parameter. When user-input is necessary to craft the
      request, it is recommended to follow OWASP best practices to prevent
      abuse, including using an allowlist.
    options:
      interfile: true
    metadata:
      cwe:
        - "CWE-918: Server-Side Request Forgery (SSRF)"
      owasp:
        - A10:2021 - Server-Side Request Forgery (SSRF)
      references:
        - https://goteleport.com/blog/ssrf-attacks/
      category: security
      technology:
        - go
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
      confidence: HIGH
      cwe2022-top25: true
      cwe2021-top25: true
      subcategory:
        - vuln
      impact: MEDIUM
      likelihood: MEDIUM
      interfile: true
      vulnerability_class:
        - Server-Side Request Forgery (SSRF)
    mode: taint
    pattern-sources:
      - label: INPUT
        patterns:
          - pattern-either:
              - pattern: |
                  ($REQUEST : *http.Request).$ANYTHING
              - pattern: |
                  ($REQUEST : http.Request).$ANYTHING
          - metavariable-regex:
              metavariable: $ANYTHING
              regex: ^(BasicAuth|Body|Cookie|Cookies|Form|FormValue|GetBody|Host|MultipartReader|ParseForm|ParseMultipartForm|PostForm|PostFormValue|Referer|RequestURI|Trailer|TransferEncoding|UserAgent|URL)$
      - label: CLEAN
        requires: INPUT
        patterns:
          - pattern-either:
              - pattern: |
                  "$URLSTR" + $INPUT
              - patterns:
                  - pattern-either:
                      - pattern: fmt.Fprintf($F, "$URLSTR", $INPUT, ...)
                      - pattern: fmt.Sprintf("$URLSTR", $INPUT, ...)
                      - pattern: fmt.Printf("$URLSTR", $INPUT, ...)
          - metavariable-regex:
              metavariable: $URLSTR
              regex: .*//[a-zA-Z0-10]+\..*
    pattern-sinks:
      - requires: INPUT and not CLEAN
        patterns:
          - pattern-either:
              - patterns:
                  - pattern-either:
                      - patterns:
                          - pattern-inside: |
                              $CLIENT := &http.Client{...}
                              ...
                          - pattern: $CLIENT.$METHOD($URL, ...)
                      - pattern: http.$METHOD($URL, ...)
                  - metavariable-regex:
                      metavariable: $METHOD
                      regex: ^(Get|Head|Post|PostForm)$
              - patterns:
                  - pattern: |
                      http.NewRequest("$METHOD", $URL, ...)
                  - metavariable-regex:
                      metavariable: $METHOD
                      regex: ^(GET|HEAD|POST|POSTFORM)$
          - focus-metavariable: $URL
    severity: WARNING

Examples

tainted-url-host.go

package main

import (
	"crypto/tls"
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"net/http"
)

func handlerIndexFmt(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		url := fmt.Sprintf("https://%v/api", r.URL.Query().Get("proxy"))

		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = fmt.Sprintf("https://%s", proxy)
		} else {
			url = fmt.Sprintf("http://%q", proxy)
		}
		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func handlerOtherFmt(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		url := fmt.Printf("https://%v/api", r.URL.Query().Get("proxy"))

		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = fmt.Fprintf(w, "https://%s", proxy)
		} else {
			url = fmt.Fprintf(w, "http://%q", proxy)
		}
		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func handlerOkFmt(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		url := fmt.Printf("https://example.com/%v", r.URL.Query().Get("proxy"))

		// ok: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = fmt.Sprintf("https://example.com/%s", proxy)
		} else {
			url = fmt.Fprintf(w, "http://example.com%q", proxy)
		}
		// ok: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func (s *server) handlerBadFmt(w http.ResponseWriter, r *http.Request) {
	urls, ok := r.URL.Query()["url"] // extract url from query params

	if !ok {
		http.Error(w, "url missing", 500)
		return
	}

	if len(urls) != 1 {
		http.Error(w, "url missing", 500)
		return
	}

	url := fmt.Sprintf("//%s/path", urls[0])

	// ruleid: tainted-url-host
	resp, err := http.Get(url) // sink
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	client := &http.Client{}

	// ruleid: tainted-url-host
	req2, err := http.NewRequest("GET", url, nil)
	_, err2 := client.Do(req2)
	if err2 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// ok: tainted-url-host
	_, err3 := http.Get("https://semgrep.dev")
	if err3 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	url4 := fmt.Sprintf("ftps://%s/path/to/%s", "test", r.URL.Path)
	// ok: tainted-url-host
	_, err4 := http.Get("https://semgrep.dev")
	if err3 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	defer resp.Body.Close()

	bytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// Write out the hexdump of the bytes as plaintext.
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprint(w, hex.Dump(bytes))
}

func handlerIndexAdd(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		url := "https://" + r.URL.Query().Get("proxy") + "/api"

		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = "https://" + proxy
		} else {
			url = "http://" + proxy
		}
		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func handlerOtherAdd(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		url := "https://" + r.URL.Query().Get("proxy") + "/api"

		// ruleid: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = "https://example.com/" + proxy
		} else {
			url = "http://example.com/api/test/" + proxy
		}
		// ok: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func handlerOkAdd(w http.ResponseWriter, r *http.Request) {
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}

	client := &http.Client{Transport: tr}

	if r.Method == "POST" && r.URL.Path == "/api" {
		// ok: tainted-url-host
		resp, err := client.Post("https://example.com/"+r.URL.Query().Get("proxy"), "application/json", r.Body)

		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		defer resp.Body.Close()

		if resp.StatusCode != 200 {
			w.WriteHeader(500)
			return
		}

		w.Write([]byte(fmt.Sprintf("{\"host\":\"%v\"}", r.URL.Query().Get("proxy"))))
		return
	} else {
		proxy := r.URL.Query()["proxy"]
		secure := r.URL.Query()["secure"]

		url := ""
		if secure {
			url = "https://example.com/" + proxy
		} else {
			url = "http://example.com" + proxy
		}
		// ok: tainted-url-host
		resp, err := client.Post(url, "application/json", r.Body)
	}
}

func (s *server) handlerBadAdd(w http.ResponseWriter, r *http.Request) {
	urls, ok := r.URL.Query()["url"] // extract url from query params

	if !ok {
		http.Error(w, "url missing", 500)
		return
	}

	if len(urls) != 1 {
		http.Error(w, "url missing", 500)
		return
	}

	url := urls[0]

	// ruleid: tainted-url-host
	resp, err := http.Get(url) // sink
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	client := &http.Client{}

	// ruleid: tainted-url-host
	req2, err := http.NewRequest("GET", r.URL.Path, nil)
	_, err2 := client.Do(req2)
	if err2 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// ok: tainted-url-host
	_, err3 := http.Get("https://semgrep.dev")
	if err3 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	url4 := fmt.Sprintf("ftps://%s/path/to/%s", "test", r.URL.Path)
	// ok: tainted-url-host
	_, err4 := http.Get("https://semgrep.dev")
	if err3 != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	defer resp.Body.Close()

	bytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	// Write out the hexdump of the bytes as plaintext.
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	fmt.Fprint(w, hex.Dump(bytes))
}

func main() {
	http.HandleFunc("/", handlerIndex)
	http.HandleFunc("/other", handleOther)
	http.HandleFunc("/ok", handleOk)
	http.HandleFunc("/bad", handlerBad)
	http.ListenAndServe(":8888", nil)
}